use super::{AgentRunner, RunnerSettings};
use crate::config::VTCodeConfig;
use crate::config::constants::tools;
use crate::config::models::ModelId;
use crate::config::types::CapabilityLevel;
use crate::core::agent::events::ExecEventRecorder;
use crate::core::agent::runtime::AgentRuntime;
use crate::core::agent::session::AgentSessionState;
use crate::core::agent::state::record_turn_duration;
use crate::core::agent::steering::SteeringMessage;
use crate::core::agent::task::{Task, TaskOutcome, TaskResults};
use crate::core::agent::types::AgentType;
use crate::core::threads::ThreadBootstrap;
use crate::exec::events::{
    HarnessEventKind, ItemCompletedEvent, ThreadEvent, ThreadItemDetails, ToolCallStatus,
};
use crate::llm::provider::{
    FinishReason, LLMError, LLMProvider, LLMRequest, LLMResponse, ToolCall,
};
use crate::tools::Tool;
use crate::tools::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
use crate::tools::handlers::{PlanModeState, TaskTrackerTool};
use crate::tools::handlers::{SessionSurface, SessionToolsConfig, ToolModelCapabilities};
use async_trait::async_trait;
use parking_lot::Mutex;
use serde_json::json;
use std::collections::VecDeque;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tempfile::TempDir;

#[test]
fn record_turn_duration_records_once() {
    let mut durations = Vec::with_capacity(5);
    let mut total_ms = 0u128;
    let mut max_ms = 0u128;
    let mut count = 0usize;
    let mut recorded = false;
    let start = Instant::now();

    record_turn_duration(
        &mut durations,
        &mut total_ms,
        &mut max_ms,
        &mut count,
        &mut recorded,
        &start,
    );
    record_turn_duration(
        &mut durations,
        &mut total_ms,
        &mut max_ms,
        &mut count,
        &mut recorded,
        &start,
    );

    assert_eq!(durations.len(), 1);
    assert_eq!(count, 1);
}

#[test]
fn finalize_outcome_marks_success() {
    let mut state = AgentSessionState::new("test".to_string(), 10, 5, 10000);
    state.is_completed = true;
    state.stats.turns_executed = 2;

    state.finalize_outcome(4);

    assert_eq!(state.outcome, TaskOutcome::Success);
}

#[test]
fn finalize_outcome_turn_limit() {
    let mut state = AgentSessionState::new("test".to_string(), 6, 5, 10000);
    state.stats.turns_executed = 6;

    state.finalize_outcome(6);

    assert!(matches!(
        state.outcome,
        TaskOutcome::TurnLimitReached { .. }
    ));
}

#[test]
fn finalize_outcome_tool_loop_limit() {
    let mut state = AgentSessionState::new("test".to_string(), 10, 2, 10000);
    state.stats.turns_executed = 2;
    state.tool_loop_limit_hit = true;

    state.finalize_outcome(10);

    assert_eq!(
        state.outcome,
        TaskOutcome::tool_loop_limit_reached(
            state.constraints.max_tool_loops,
            state.consecutive_tool_loops
        )
    );
}

#[test]
fn into_results_computes_metrics() {
    let mut state = AgentSessionState::new("test".to_string(), 5, 5, 10000);
    state.turn_durations_ms = vec![100, 200, 300];
    state.turn_total_ms = 600;
    state.turn_max_ms = 300;
    state.turn_count = 3;
    state.stats.turns_executed = 3;
    state.outcome = TaskOutcome::Success;
    state.modified_files = vec!["file.rs".to_owned()];
    state.executed_commands = vec!["write_file".to_owned()];
    state.warnings = vec!["warning".to_owned()];

    let total_duration_ms = 1_000u128;
    let results = state.into_results("summary".to_owned(), Vec::new(), total_duration_ms);

    assert_eq!(results.outcome, TaskOutcome::Success);
    assert_eq!(results.turns_executed, 3);
    assert_eq!(results.total_duration_ms, total_duration_ms);
    assert_eq!(results.max_turn_duration_ms, Some(300));
    assert_eq!(results.average_turn_duration_ms, Some(200.0));
    assert_eq!(results.modified_files, vec!["file.rs".to_owned()]);
    assert_eq!(results.executed_commands, vec!["write_file".to_owned()]);
    assert_eq!(results.summary, "summary");
    assert_eq!(results.warnings, vec!["warning".to_owned()]);
}

#[tokio::test]
async fn full_auto_allowlist_hides_tools_from_exposure() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-allowlist".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    runner
        .enable_full_auto(&[tools::UNIFIED_FILE.to_string()])
        .await;

    assert!(runner.is_tool_exposed(tools::UNIFIED_FILE).await);
    assert!(!runner.is_tool_exposed(tools::UNIFIED_EXEC).await);
}

#[tokio::test]
async fn runner_uses_public_tool_resolution_for_validation() {
    let temp = TempDir::new().expect("tempdir");
    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-tool-validation".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    assert!(runner.is_valid_tool(tools::READ_FILE).await);
    assert!(runner.is_valid_tool("Exec code").await);
    assert!(!runner.is_valid_tool("exec_code").await);
}

#[tokio::test]
async fn build_universal_tools_matches_registry_agent_runner_snapshot() {
    let temp = TempDir::new().expect("tempdir");
    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-tool-snapshot".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    let registry_tools = runner
        .tool_registry
        .model_tools(SessionToolsConfig {
            surface: SessionSurface::AgentRunner,
            capability_level: CapabilityLevel::CodeSearch,
            documentation_mode: runner.config().agent.tool_documentation_mode,
            plan_mode: runner.tool_registry.is_plan_mode(),
            request_user_input_enabled: false,
            model_capabilities: ToolModelCapabilities::for_model_name(&runner.model),
            deferred_tool_policy: crate::tools::handlers::deferred_tool_policy_for_runtime(
                crate::llm::factory::infer_provider(
                    Some(&runner.config().agent.provider),
                    &runner.model,
                ),
                runner
                    .provider_client
                    .supports_responses_compaction(&runner.model),
                Some(runner.config()),
            ),
            anthropic_native_memory_enabled:
                crate::tools::handlers::anthropic_native_memory_enabled_for_runtime(
                    crate::llm::factory::infer_provider(
                        Some(&runner.config().agent.provider),
                        &runner.model,
                    ),
                    &runner.model,
                    Some(runner.config()),
                ),
        })
        .await;
    let mut expected = Vec::new();
    for tool in registry_tools {
        if runner.is_tool_exposed(tool.function_name()).await {
            expected.push(tool.function_name().to_string());
        }
    }

    let actual = runner
        .build_universal_tools()
        .await
        .expect("universal tools")
        .into_iter()
        .map(|tool| tool.function_name().to_string())
        .collect::<Vec<_>>();

    assert_eq!(actual, expected);
}

#[tokio::test]
async fn build_universal_tools_uses_override_when_present() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-tool-override".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    runner.set_tool_definitions_override(vec![crate::llm::provider::ToolDefinition::function(
        "only_tool".to_string(),
        "Only tool".to_string(),
        json!({ "type": "object" }),
    )]);

    let tools = runner.build_universal_tools().await.expect("tool override");

    assert_eq!(tools.len(), 1);
    assert_eq!(tools[0].function_name(), "only_tool");
}

#[tokio::test]
async fn normalize_tool_args_applies_transform_after_defaults() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-tool-transform".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");
    runner.set_tool_arg_transform(Arc::new(|name, value| {
        let mut obj = value.as_object().cloned().expect("object args");
        obj.insert("tool_name".to_string(), json!(name));
        serde_json::Value::Object(obj)
    }));

    let mut state = AgentSessionState::new("session".to_string(), 5, 5, 10_000);
    state.last_dir_path = Some(temp.path().display().to_string());
    let normalized = runner.normalize_tool_args(
        tools::UNIFIED_SEARCH,
        &json!({"action": "list"}),
        &mut state,
    );

    assert_eq!(normalized["tool_name"], tools::UNIFIED_SEARCH);
    assert_eq!(normalized["path"], json!(temp.path().display().to_string()));
}

#[tokio::test]
async fn new_with_preloaded_config_uses_override_snapshot() {
    let temp = TempDir::new().expect("tempdir");
    fs::write(
        temp.path().join("vtcode.toml"),
        "[agent]\nprovider = \"openai\"\n",
    )
    .expect("workspace config");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.provider = "anthropic".to_string();

    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-test".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(vt_cfg),
        None,
    )
    .await
    .expect("runner");

    assert_eq!(runner.core_agent_config().provider, "anthropic");
}

#[tokio::test]
async fn core_agent_config_normalizes_api_key_env_and_checkpoint_dir() {
    let temp = TempDir::new().expect("tempdir");
    let absolute_checkpoint_dir = temp.path().join("snapshots-absolute");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.provider = "minimax".to_string();
    vt_cfg.agent.api_key_env = crate::config::constants::defaults::DEFAULT_API_KEY_ENV.to_string();
    vt_cfg.agent.checkpointing.storage_dir = Some(absolute_checkpoint_dir.display().to_string());

    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-test-normalized-config".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(vt_cfg),
        None,
    )
    .await
    .expect("runner");

    let config = runner.core_agent_config();
    assert_eq!(config.api_key_env, "MINIMAX_API_KEY");
    assert_eq!(
        config.checkpointing_storage_dir,
        Some(absolute_checkpoint_dir)
    );
}

#[tokio::test]
async fn runner_uses_configured_provider_for_huggingface_repo_models() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.provider = "huggingface".to_string();
    vt_cfg.agent.default_model =
        crate::config::constants::models::huggingface::ZAI_GLM_5_NOVITA.to_string();

    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::HuggingFaceGlm5Novita,
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-huggingface-provider".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(vt_cfg),
        None,
    )
    .await
    .expect("runner");

    assert_eq!(runner.provider_client.name(), "huggingface");
}

#[tokio::test]
async fn review_tool_allowlist_excludes_mutating_and_plan_only_tools() {
    let temp = TempDir::new().expect("tempdir");
    let runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-review".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    let allowlist = runner
        .review_tool_allowlist(&[
            tools::UNIFIED_FILE.to_string(),
            tools::UNIFIED_EXEC.to_string(),
            "task_tracker".to_string(),
            "plan_task_tracker".to_string(),
            "enter_plan_mode".to_string(),
        ])
        .await;

    assert_eq!(allowlist, vec![tools::UNIFIED_FILE.to_string()]);
}

#[tokio::test]
async fn review_tool_allowlist_expands_wildcard_read_only() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        temp.path().to_path_buf(),
        "thread-review-wildcard".to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(VTCodeConfig::default()),
        None,
    )
    .await
    .expect("runner");

    runner
        .enable_full_auto(&[tools::UNIFIED_FILE.to_string()])
        .await;

    assert!(runner.is_tool_exposed(tools::UNIFIED_FILE).await);
    assert!(!runner.is_tool_exposed(tools::UNIFIED_EXEC).await);
}

#[derive(Clone)]
struct QueuedProvider {
    responses: Arc<Mutex<VecDeque<LLMResponse>>>,
}

impl QueuedProvider {
    fn new(responses: Vec<LLMResponse>) -> Self {
        Self {
            responses: Arc::new(Mutex::new(responses.into())),
        }
    }
}

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

    async fn generate(&self, _request: LLMRequest) -> Result<LLMResponse, LLMError> {
        self.responses
            .lock()
            .pop_front()
            .ok_or(LLMError::InvalidRequest {
                message: "QueuedProvider has no queued responses".to_string(),
                metadata: None,
            })
    }

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

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

#[derive(Clone)]
struct RecordingQueuedProvider {
    responses: Arc<Mutex<VecDeque<LLMResponse>>>,
    requests: Arc<Mutex<Vec<LLMRequest>>>,
}

impl RecordingQueuedProvider {
    fn new(responses: Vec<LLMResponse>) -> Self {
        Self {
            responses: Arc::new(Mutex::new(responses.into())),
            requests: Arc::new(Mutex::new(Vec::new())),
        }
    }

    fn recorded_requests(&self) -> Vec<LLMRequest> {
        self.requests.lock().clone()
    }
}

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

    async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
        self.requests.lock().push(request);
        self.responses
            .lock()
            .pop_front()
            .ok_or(LLMError::InvalidRequest {
                message: "RecordingQueuedProvider has no queued responses".to_string(),
                metadata: None,
            })
    }

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

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

fn task(title: &str, id: &str) -> Task {
    Task {
        id: id.to_string(),
        title: title.to_string(),
        description: title.to_string(),
        instructions: None,
    }
}

fn text_response(text: &str) -> LLMResponse {
    LLMResponse {
        content: Some(text.to_string()),
        model: "gpt-5.3-codex".to_string(),
        finish_reason: FinishReason::Stop,
        ..Default::default()
    }
}

fn json_response(value: serde_json::Value) -> LLMResponse {
    text_response(&value.to_string())
}

fn tool_call_response(tool_name: &str, args: serde_json::Value) -> LLMResponse {
    LLMResponse {
        content: Some(format!("Calling {tool_name}")),
        tool_calls: Some(vec![ToolCall::function(
            "call-1".to_string(),
            tool_name.to_string(),
            args.to_string(),
        )]),
        model: "gpt-5.3-codex".to_string(),
        finish_reason: FinishReason::ToolCalls,
        ..Default::default()
    }
}

fn planner_response_json(verify_command: &str) -> serde_json::Value {
    json!({
        "spec_markdown": "# Execution Spec\n\nImplement the requested change with a resumable tracker.",
        "contract_markdown": format!(
            "# Execution Contract\n\n## Done Criteria\n- Implement the requested change.\n- Verify with `{}`.\n",
            verify_command
        ),
        "task_title": "Planner seeded task",
        "items": [{
            "description": "Implement the requested change",
            "outcome": "The requested change is implemented and tracked.",
            "verify": [verify_command],
        }]
    })
}

fn evaluator_response_json(
    verdict: &str,
    summary: &str,
    high_severity_findings: usize,
) -> serde_json::Value {
    let scores = if verdict.eq_ignore_ascii_case("pass") && high_severity_findings == 0 {
        (5, 5, 5, 5)
    } else {
        (3, 2, 4, 3)
    };
    evaluator_response_json_with_scorecard(verdict, summary, high_severity_findings, scores)
}

fn evaluator_response_json_with_scorecard(
    verdict: &str,
    summary: &str,
    high_severity_findings: usize,
    scores: (u8, u8, u8, u8),
) -> serde_json::Value {
    json!({
        "verdict": verdict,
        "summary": summary,
        "high_severity_findings": high_severity_findings,
        "scorecard": {
            "contract_fidelity": scores.0,
            "functionality": scores.1,
            "code_quality": scores.2,
            "verification_integrity": scores.3,
        },
        "findings": [{
            "severity": if high_severity_findings > 0 { "high" } else { "low" },
            "title": summary,
            "detail": summary,
        }],
        "unmet_contract_items": [],
        "residual_risks": [],
        "required_tracker_updates": [],
    })
}

fn tool_call_response_with_request_id(
    tool_name: &str,
    args: serde_json::Value,
    request_id: &str,
) -> LLMResponse {
    let mut response = tool_call_response(tool_name, args);
    response.request_id = Some(request_id.to_string());
    response
}

fn workspace_root(temp: &TempDir) -> PathBuf {
    temp.path()
        .canonicalize()
        .unwrap_or_else(|_| temp.path().to_path_buf())
}

async fn make_runner(temp: &TempDir, vt_cfg: VTCodeConfig, session_id: &str) -> AgentRunner {
    let mut runner = AgentRunner::new_with_bootstrap(
        AgentType::Single,
        ModelId::default(),
        "test-key".to_string(),
        workspace_root(temp),
        session_id.to_string(),
        RunnerSettings {
            reasoning_effort: None,
            verbosity: None,
        },
        None,
        ThreadBootstrap::new(None),
        Some(vt_cfg),
        None,
    )
    .await
    .expect("runner");
    runner.set_quiet(true);
    runner
}

async fn seed_tracker(workspace_root: &Path, items: serde_json::Value) {
    let tool = TaskTrackerTool::new(
        workspace_root.to_path_buf(),
        PlanModeState::new(workspace_root.to_path_buf()),
    );
    tool.execute(json!({
        "action": "create",
        "title": "Harness hardening",
        "items": items,
    }))
    .await
    .expect("seed tracker");
}

fn turn_started_count(results: &TaskResults) -> usize {
    results
        .thread_events
        .iter()
        .filter(|event| matches!(event, ThreadEvent::TurnStarted(_)))
        .count()
}

fn harness_events(results: &TaskResults) -> Vec<HarnessEventKind> {
    results
        .thread_events
        .iter()
        .filter_map(|event| match event {
            ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => match &item.details {
                ThreadItemDetails::Harness(harness) => Some(harness.event.clone()),
                _ => None,
            },
            _ => None,
        })
        .collect()
}

fn harness_paths(results: &TaskResults, kind: HarnessEventKind) -> Vec<String> {
    results
        .thread_events
        .iter()
        .filter_map(|event| match event {
            ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => match &item.details {
                ThreadItemDetails::Harness(harness) if harness.event == kind => {
                    harness.path.clone()
                }
                _ => None,
            },
            _ => None,
        })
        .collect()
}

fn completed_tool_invocation_item_id(events: &[ThreadEvent], tool_call_id: &str) -> Option<String> {
    events.iter().find_map(|event| match event {
        ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => match &item.details {
            ThreadItemDetails::ToolInvocation(details)
                if details.tool_call_id.as_deref() == Some(tool_call_id) =>
            {
                Some(item.id.clone())
            }
            _ => None,
        },
        _ => None,
    })
}

fn completed_tool_output_count(
    events: &[ThreadEvent],
    tool_call_id: &str,
    status: ToolCallStatus,
    call_item_id: &str,
) -> usize {
    events
        .iter()
        .filter(|event| match event {
            ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) => match &item.details {
                ThreadItemDetails::ToolOutput(details) => {
                    details.tool_call_id.as_deref() == Some(tool_call_id)
                        && details.status == status
                        && details.call_id == call_item_id
                }
                _ => false,
            },
            _ => false,
        })
        .count()
}

#[tokio::test]
async fn exec_full_auto_continues_until_tracker_is_completed() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);
    seed_tracker(&workspace, json!(["Finish tracker step"])).await;

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::Single;
    vt_cfg.automation.full_auto.max_turns = 3;
    let mut runner = make_runner(&temp, vt_cfg, "thread-continuation-success").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        text_response("The task is complete."),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("I have finished all the work."),
    ]));

    let result = runner
        .execute_task(&task("Harness continuation", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(result.turns_executed > 1);
    assert!(harness_events(&result).contains(&HarnessEventKind::ContinuationStarted));

    let tracker =
        fs::read_to_string(workspace.join(".vtcode/tasks/current_task.md")).expect("tracker file");
    assert!(tracker.contains("- [x] Finish tracker step"));
}

#[tokio::test]
async fn runner_reuses_openai_response_chain_and_session_cache_key() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);
    seed_tracker(&workspace, json!(["Cache-aware tracker step"])).await;

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::Single;
    let mut runner = make_runner(&temp, vt_cfg, "thread-cache-lineage").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    let provider = RecordingQueuedProvider::new(vec![
        tool_call_response_with_request_id(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
            "resp_first_turn",
        ),
        text_response("All work is complete."),
        text_response("All work is complete."),
        text_response("All work is complete."),
    ]);
    let recorded = provider.clone();
    runner.provider_client = Box::new(provider);

    let result = runner
        .execute_task(&task("Cache-aware continuation", "exec-task"), &[])
        .await
        .expect("task result");

    assert!(result.turns_executed >= 2);

    let requests = recorded.recorded_requests();
    assert!(requests.len() >= 2);
    assert_eq!(requests[0].previous_response_id, None);
    assert_eq!(
        requests[0].prompt_cache_key.as_deref(),
        Some("vtcode:openai:thread-cache-lineage")
    );
    assert_eq!(
        requests[1].previous_response_id.as_deref(),
        Some("resp_first_turn")
    );
    assert_eq!(requests[1].prompt_cache_key, requests[0].prompt_cache_key);
}

#[tokio::test]
async fn exec_full_auto_runs_verification_before_accepting_completion() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);
    seed_tracker(
        &workspace,
        json!([{
            "description": "Verify harness",
            "status": "completed",
            "verify": "pwd",
        }]),
    )
    .await;

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::Single;
    let mut runner = make_runner(&temp, vt_cfg, "thread-verification-success").await;
    runner.enable_full_auto(&[]).await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![text_response(
        "The task is complete.",
    )]));

    let result = runner
        .execute_task(&task("Verification success", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(result.turns_executed >= 1);
    assert_eq!(turn_started_count(&result), 1);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::VerificationStarted));
    assert!(events.contains(&HarnessEventKind::VerificationPassed));
}

#[tokio::test]
async fn exec_full_auto_retries_after_verification_failure() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);
    seed_tracker(
        &workspace,
        json!([{
            "description": "Verify harness",
            "status": "completed",
            "verify": "cat missing-verification-target",
        }]),
    )
    .await;

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::Single;
    vt_cfg.automation.full_auto.max_turns = 2;
    let mut runner = make_runner(&temp, vt_cfg, "thread-verification-failure").await;
    runner.enable_full_auto(&[]).await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        text_response("The task is complete."),
        text_response("Task is now complete."),
    ]));

    let result = runner
        .execute_task(&task("Verification failure", "exec-task"), &[])
        .await
        .expect("task result");

    assert!(matches!(
        result.outcome,
        TaskOutcome::TurnLimitReached { .. }
    ));
    assert!(result.turns_executed > 1);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::VerificationStarted));
    assert!(events.contains(&HarnessEventKind::VerificationFailed));
    assert!(events.contains(&HarnessEventKind::ContinuationStarted));
}

#[tokio::test]
async fn review_runs_skip_continuation_and_finish_single_pass() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = make_runner(&temp, VTCodeConfig::default(), "thread-review-skip").await;
    runner
        .enable_full_auto(&[tools::UNIFIED_FILE.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![text_response(
        "The task is complete.",
    )]));

    let result = runner
        .execute_task(&task("Review task", "review-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(result.turns_executed >= 1);
    assert_eq!(turn_started_count(&result), 1);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::ContinuationSkipped));
    assert!(!events.contains(&HarnessEventKind::ContinuationStarted));
}

#[tokio::test]
async fn plan_mode_runs_skip_continuation_and_finish_single_pass() {
    let temp = TempDir::new().expect("tempdir");
    let mut runner = make_runner(&temp, VTCodeConfig::default(), "thread-plan-mode-skip").await;
    runner.enable_full_auto(&[]).await;
    runner.enable_plan_mode();
    runner.provider_client = Box::new(QueuedProvider::new(vec![text_response(
        "The task is complete.",
    )]));

    let result = runner
        .execute_task(&task("Plan mode task", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(result.turns_executed >= 1);
    assert_eq!(turn_started_count(&result), 1);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::ContinuationSkipped));
    assert!(!events.contains(&HarnessEventKind::ContinuationStarted));
}

#[tokio::test]
async fn exec_only_policy_skips_when_full_auto_is_disabled() {
    let temp = TempDir::new().expect("tempdir");
    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.continuation_policy =
        vtcode_config::core::agent::ContinuationPolicy::ExecOnly;
    let mut runner = make_runner(&temp, vt_cfg, "thread-exec-only-skip").await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![text_response(
        "The task is complete.",
    )]));

    let result = runner
        .execute_task(&task("Exec task", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(result.turns_executed >= 1);
    assert_eq!(turn_started_count(&result), 1);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::ContinuationSkipped));
    assert!(!events.contains(&HarnessEventKind::ContinuationStarted));
}

#[tokio::test]
async fn tool_loop_limit_writes_blocked_handoff_artifacts() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);
    seed_tracker(&workspace, json!(["Investigate loop"])).await;

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::Single;
    vt_cfg.automation.full_auto.max_turns = 1;
    vt_cfg.tools.max_tool_loops = 1;
    let mut runner = make_runner(&temp, vt_cfg, "thread-tool-loop-blocked").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![tool_call_response(
        tools::TASK_TRACKER,
        json!({
            "action": "list"
        }),
    )]));

    let result = runner
        .execute_task(&task("Loop blocked", "exec-task"), &[])
        .await
        .expect("task result");

    assert!(matches!(
        result.outcome,
        TaskOutcome::ToolLoopLimitReached { .. }
    ));
    let paths = harness_paths(&result, HarnessEventKind::BlockedHandoffWritten);
    assert_eq!(paths.len(), 2);
    for path in paths {
        let content = fs::read_to_string(&path).expect("blocked handoff file");
        assert!(content.contains("tool_loop_limit_reached"));
        assert!(content.contains("Stopped after reaching tool loop limit"));
    }
}

#[tokio::test]
async fn plan_build_evaluate_exec_creates_spec_and_evaluation_artifacts() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    let mut runner = make_runner(&temp, vt_cfg, "thread-plan-build-evaluate").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "Evaluator accepted the implementation.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(&task("Planner + evaluator", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(
        workspace.join(".vtcode/tasks/current_spec.md").exists(),
        "planner should write current_spec.md"
    );
    assert!(
        workspace.join(".vtcode/tasks/current_contract.md").exists(),
        "planner should write current_contract.md"
    );
    assert!(
        workspace
            .join(".vtcode/tasks/current_evaluation.md")
            .exists(),
        "evaluator should write current_evaluation.md"
    );
    let tracker =
        fs::read_to_string(workspace.join(".vtcode/tasks/current_task.md")).expect("tracker file");
    assert!(tracker.contains("outcome: The requested change is implemented and tracked."));
    assert!(tracker.contains("verify: pwd"));
    let contract = fs::read_to_string(workspace.join(".vtcode/tasks/current_contract.md"))
        .expect("contract file");
    assert!(contract.contains("Execution Contract"));
    assert!(contract.contains("Verify with `pwd`"));

    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::PlanningStarted));
    assert!(events.contains(&HarnessEventKind::PlanningCompleted));
    assert!(events.contains(&HarnessEventKind::EvaluationStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));
}

#[tokio::test]
async fn default_full_auto_exec_uses_plan_build_evaluate_harness() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);

    let vt_cfg = VTCodeConfig::default();
    let mut runner = make_runner(&temp, vt_cfg, "thread-default-plan-build-evaluate").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "Evaluator accepted the implementation.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(&task("Default planner + evaluator", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    assert!(
        workspace.join(".vtcode/tasks/current_spec.md").exists(),
        "default full-auto should write current_spec.md"
    );
    assert!(
        workspace.join(".vtcode/tasks/current_contract.md").exists(),
        "default full-auto should write current_contract.md"
    );
    assert!(
        workspace
            .join(".vtcode/tasks/current_evaluation.md")
            .exists(),
        "default full-auto should write current_evaluation.md"
    );

    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::PlanningStarted));
    assert!(events.contains(&HarnessEventKind::PlanningCompleted));
    assert!(events.contains(&HarnessEventKind::EvaluationStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));
}

#[tokio::test]
async fn evaluator_failure_forces_revision_before_success() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    vt_cfg.automation.full_auto.max_turns = 4;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-revision").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json(
            "fail",
            "A high-severity issue remains.",
            1,
        )),
        text_response("Revision 1: task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "All issues have been addressed.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(&task("Evaluator revision", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::EvaluationFailed));
    assert!(events.contains(&HarnessEventKind::RevisionStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));
}

#[tokio::test]
async fn evaluator_request_includes_verification_results() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-verification").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    let provider = RecordingQueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "Verification evidence looks good.",
            0,
        )),
    ]);
    let recorded = provider.clone();
    runner.provider_client = Box::new(provider);

    let result = runner
        .execute_task(&task("Evaluator verification evidence", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);

    let requests = recorded.recorded_requests();
    let evaluator_request = requests.last().expect("evaluator request");
    let evaluator_prompt = evaluator_request
        .messages
        .first()
        .map(|message| message.content.as_text().into_owned())
        .expect("evaluator prompt");
    assert!(evaluator_prompt.contains("Current contract:"));
    assert!(evaluator_prompt.contains("Verification results:"));
    assert!(evaluator_prompt.contains("[PASS] pwd (exit 0)"));
    assert!(evaluator_prompt.contains("contract_fidelity"));
}

#[tokio::test]
async fn evaluator_scorecard_below_threshold_forces_revision() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    vt_cfg.automation.full_auto.max_turns = 4;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-scorecard").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json_with_scorecard(
            "pass",
            "Looks mostly good.",
            0,
            (5, 3, 5, 5),
        )),
        text_response("Revision 1: task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "All issues have been addressed.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(&task("Evaluator scorecard revision", "exec-task"), &[])
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::EvaluationFailed));
    assert!(events.contains(&HarnessEventKind::RevisionStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));

    let evaluation = fs::read_to_string(temp.path().join(".vtcode/tasks/current_evaluation.md"))
        .expect("evaluation file");
    assert!(evaluation.contains("## Scorecard"));
    assert!(evaluation.contains("Functionality: 5/5"));
}

#[tokio::test]
async fn evaluator_missing_scorecard_forces_revision() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    vt_cfg.automation.full_auto.max_turns = 4;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-missing-scorecard").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(json!({
            "verdict": "pass",
            "summary": "Looks mostly good.",
            "high_severity_findings": 0,
            "findings": [],
            "unmet_contract_items": [],
            "residual_risks": [],
            "required_tracker_updates": [],
        })),
        text_response("Revision 1: task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "All issues have been addressed.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(
            &task("Evaluator missing scorecard revision", "exec-task"),
            &[],
        )
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::EvaluationFailed));
    assert!(events.contains(&HarnessEventKind::RevisionStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));

    let evaluation = fs::read_to_string(temp.path().join(".vtcode/tasks/current_evaluation.md"))
        .expect("evaluation file");
    assert!(evaluation.contains("All issues have been addressed."));
}

#[tokio::test]
async fn evaluator_out_of_range_scorecard_forces_revision() {
    let temp = TempDir::new().expect("tempdir");

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    vt_cfg.automation.full_auto.max_turns = 4;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-invalid-scorecard").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json_with_scorecard(
            "pass",
            "Looks mostly good.",
            0,
            (5, 9, 5, 5),
        )),
        text_response("Revision 1: task is complete."),
        json_response(evaluator_response_json(
            "pass",
            "All issues have been addressed.",
            0,
        )),
    ]));

    let result = runner
        .execute_task(
            &task("Evaluator invalid scorecard revision", "exec-task"),
            &[],
        )
        .await
        .expect("task result");

    assert_eq!(result.outcome, TaskOutcome::Success);
    let events = harness_events(&result);
    assert!(events.contains(&HarnessEventKind::EvaluationFailed));
    assert!(events.contains(&HarnessEventKind::RevisionStarted));
    assert!(events.contains(&HarnessEventKind::EvaluationPassed));

    let evaluation = fs::read_to_string(temp.path().join(".vtcode/tasks/current_evaluation.md"))
        .expect("evaluation file");
    assert!(evaluation.contains("All issues have been addressed."));
}

#[tokio::test]
async fn evaluator_exhaustion_writes_blocked_handoff_with_artifact_paths() {
    let temp = TempDir::new().expect("tempdir");
    let workspace = workspace_root(&temp);

    let mut vt_cfg = VTCodeConfig::default();
    vt_cfg.agent.harness.orchestration_mode =
        vtcode_config::core::agent::HarnessOrchestrationMode::PlanBuildEvaluate;
    vt_cfg.agent.harness.max_revision_rounds = 1;
    vt_cfg.automation.full_auto.max_turns = 4;
    let mut runner = make_runner(&temp, vt_cfg, "thread-evaluator-exhaustion").await;
    runner
        .enable_full_auto(&[tools::TASK_TRACKER.to_string()])
        .await;
    runner.provider_client = Box::new(QueuedProvider::new(vec![
        json_response(planner_response_json("pwd")),
        tool_call_response(
            tools::TASK_TRACKER,
            json!({
                "action": "update",
                "index": 1,
                "status": "completed",
            }),
        ),
        text_response("The task is complete."),
        json_response(evaluator_response_json(
            "fail",
            "First evaluator rejection.",
            1,
        )),
        text_response("Revision 1: task is complete."),
        json_response(evaluator_response_json(
            "fail",
            "Second evaluator rejection.",
            1,
        )),
    ]));

    let result = runner
        .execute_task(&task("Evaluator exhaustion", "exec-task"), &[])
        .await
        .expect("task result");

    assert!(matches!(result.outcome, TaskOutcome::Failed { .. }));
    let paths = harness_paths(&result, HarnessEventKind::BlockedHandoffWritten);
    assert_eq!(paths.len(), 2);
    for path in paths {
        let content = fs::read_to_string(&path).expect("blocked handoff file");
        assert!(content.contains("current_spec.md"));
        assert!(content.contains("current_contract.md"));
        assert!(content.contains("current_evaluation.md"));
    }
    assert!(workspace.join(".vtcode/tasks/current_spec.md").exists());
    assert!(workspace.join(".vtcode/tasks/current_contract.md").exists());
    assert!(
        workspace
            .join(".vtcode/tasks/current_evaluation.md")
            .exists()
    );
}

#[path = "tests/tool_execution.rs"]
mod tool_execution;
