use anyhow::Result;
use vtcode_core::llm::providers::split_reasoning_from_text;
use vtcode_core::utils::ansi::AnsiRenderer;
use vtcode_core::utils::ansi::MessageStyle;

use crate::agent::runloop::unified::plan_blocks::extract_any_plan;
use crate::agent::runloop::unified::turn::context::{
    PreparedAssistantToolCall, TurnProcessingResult,
};
use crate::agent::runloop::unified::turn::guards::validate_tool_args_security;

/// Process an LLM response and return a `TurnProcessingResult` describing whether
/// there are tool calls to run, a textual assistant response, or nothing.
#[expect(clippy::too_many_arguments)]
pub(crate) fn process_llm_response(
    response: &vtcode_core::llm::provider::LLMResponse,
    renderer: &mut AnsiRenderer,
    conversation_len: usize,
    plan_mode_active: bool,
    allow_plan_interview: bool,
    request_user_input_enabled: bool,
    allow_tool_calls: bool,
    validation_cache: Option<
        &std::sync::Arc<vtcode_core::tools::validation_cache::ValidationCache>,
    >,
    tool_registry: Option<&vtcode_core::tools::ToolRegistry>,
) -> Result<TurnProcessingResult> {
    use crate::agent::runloop::unified::turn::harmony::strip_harmony_syntax;
    use vtcode_core::config::constants::tools;
    use vtcode_core::llm::provider as uni;

    let reasoning = split_reasoning_from_text(response.reasoning.as_deref().unwrap_or("")).0;
    let reasoning_details = response.reasoning_details.clone();
    let mut final_text = response.content.clone();
    let mut proposed_plan: Option<String> = None;
    let mut tool_calls = if allow_tool_calls {
        response.tool_calls.clone().unwrap_or_default()
    } else {
        Vec::new()
    };
    let mut interpreted_textual_call = false;
    let mut is_harmony = false;
    let reasoning_segment_count = reasoning.len();
    let reasoning_details_count = reasoning_details.as_ref().map_or(0, Vec::len);

    if let Some(ref text) = final_text
        && (text.contains("<|start|>") || text.contains("<|channel|>") || text.contains("<|call|>"))
    {
        is_harmony = true;
        let cleaned = strip_harmony_syntax(text);
        if !cleaned.trim().is_empty() {
            final_text = Some(cleaned);
        } else {
            final_text = Some("".to_string());
        }
    }

    if plan_mode_active
        && tool_calls.is_empty()
        && let Some(ref text) = final_text
    {
        let extraction = extract_any_plan(text);
        final_text = Some(
            text.replace("<proposed_plan>", "")
                .replace("</proposed_plan>", "")
                .replace("<plan>", "")
                .replace("</plan>", ""),
        );
        proposed_plan = extraction.plan_text;
    }

    // Strip DSML markup from text before rendering to prevent raw tags
    // from leaking into the user-visible output.
    if let Some(ref text) = final_text
        && (text.contains("<\u{ff5c}\u{ff5c}DSML\u{ff5c}\u{ff5c}")
            || text.contains("</\u{ff5c}\u{ff5c}DSML\u{ff5c}\u{ff5c}"))
    {
        final_text = Some(crate::agent::runloop::text_tools::strip_dsml_markup(text));
    }

    if allow_tool_calls
        && tool_calls.is_empty()
        && let Some(text) = final_text.as_deref()
        && !text.trim().is_empty()
        && let Some((name, args)) =
            crate::agent::runloop::text_tools::detect_textual_tool_call(text)
    {
        if let Some(validation_failures) =
            validate_tool_args_security(&name, &args, validation_cache, tool_registry)
        {
            let tool_display =
                crate::agent::runloop::unified::tool_summary::humanize_tool_name(&name);
            let failures_list = validation_failures.join("; ");
            crate::agent::runloop::unified::turn::turn_helpers::display_status(
                renderer,
                &format!(
                    "Detected {} but validation failed: {}",
                    tool_display, failures_list
                ),
            )?;
        } else {
            let args_json = serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string());
            let code_blocks = crate::agent::runloop::text_tools::extract_code_fence_blocks(text);
            if !code_blocks.is_empty() {
                crate::agent::runloop::tool_output::render_code_fence_blocks(
                    renderer,
                    &code_blocks,
                )?;
                renderer.line(MessageStyle::Output, "")?;
            }
            let (headline, _) =
                crate::agent::runloop::unified::tool_summary::describe_tool_action(&name, &args);
            let notice = if headline.is_empty() {
                format!(
                    "Detected {} request",
                    crate::agent::runloop::unified::tool_summary::humanize_tool_name(&name)
                )
            } else {
                format!("Detected {headline}")
            };
            crate::agent::runloop::unified::turn::turn_helpers::display_status(renderer, &notice)?;
            let call_id = format!("call_textual_{}", conversation_len);
            tool_calls.push(uni::ToolCall::function(
                call_id.clone(),
                name.clone(),
                args_json.clone(),
            ));
            interpreted_textual_call = true;
            final_text = None;
        }
    }

    if allow_tool_calls
        && !interpreted_textual_call
        && allow_plan_interview
        && request_user_input_enabled
        && tool_calls.is_empty()
        && let Some(text) = final_text.as_deref()
        && let Some(args) = build_interview_args_from_text(text)
    {
        let args_json = serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string());
        let call_id = format!("call_interview_{}", conversation_len);
        tool_calls.push(uni::ToolCall::function(
            call_id.clone(),
            tools::REQUEST_USER_INPUT.to_string(),
            args_json,
        ));
        interpreted_textual_call = true;
        final_text = None;
    }

    if !tool_calls.is_empty() {
        tracing::info!(
            target: "vtcode.turn.metrics",
            metric = "llm_response_parsed",
            kind = "tool_calls",
            tool_calls = tool_calls.len(),
            interpreted_textual_call,
            reasoning_segments = reasoning_segment_count,
            reasoning_details = reasoning_details_count,
            content_len = final_text.as_ref().map_or(0, |text| text.len()),
            is_harmony,
            plan_mode_active,
            allow_plan_interview,
            request_user_input_enabled,
            proposed_plan = proposed_plan.is_some(),
            "turn metric"
        );
        return Ok(TurnProcessingResult::ToolCalls {
            tool_calls: prepare_tool_calls(tool_calls),
            assistant_text: if interpreted_textual_call {
                String::new()
            } else {
                final_text.unwrap_or_default()
            },
            reasoning,
            reasoning_details,
        });
    }

    if let Some(text) = final_text
        && (!text.trim().is_empty() || is_harmony || proposed_plan.is_some())
    {
        tracing::info!(
            target: "vtcode.turn.metrics",
            metric = "llm_response_parsed",
            kind = "text",
            tool_calls = 0,
            interpreted_textual_call,
            reasoning_segments = reasoning_segment_count,
            reasoning_details = reasoning_details_count,
            content_len = text.len(),
            is_harmony,
            plan_mode_active,
            allow_plan_interview,
            request_user_input_enabled,
            proposed_plan = proposed_plan.is_some(),
            "turn metric"
        );
        return Ok(TurnProcessingResult::TextResponse {
            text,
            reasoning,
            reasoning_details,
            proposed_plan,
        });
    }

    tracing::info!(
        target: "vtcode.turn.metrics",
        metric = "llm_response_parsed",
        kind = "empty",
        tool_calls = 0,
        interpreted_textual_call,
        reasoning_segments = reasoning_segment_count,
        reasoning_details = reasoning_details_count,
        content_len = 0,
        is_harmony,
        plan_mode_active,
        allow_plan_interview,
        request_user_input_enabled,
        proposed_plan = proposed_plan.is_some(),
        "turn metric"
    );

    Ok(TurnProcessingResult::Empty)
}

pub(crate) fn prepare_tool_calls(
    tool_calls: Vec<vtcode_core::llm::provider::ToolCall>,
) -> Vec<PreparedAssistantToolCall> {
    tool_calls
        .into_iter()
        .map(PreparedAssistantToolCall::new)
        .collect()
}

pub(crate) fn build_interview_args_from_text(text: &str) -> Option<serde_json::Value> {
    let mut questions = extract_interview_questions(text);
    if questions.is_empty()
        && let Some(synthesized) = synthesize_alignment_question(text)
    {
        questions.push(synthesized);
    }
    if questions.is_empty() {
        return None;
    }

    let focus_area = infer_focus_area(text);
    let analysis_hints = extract_analysis_hints(text);

    let payload = questions
        .iter()
        .enumerate()
        .map(|(index, question)| {
            let mut entry = serde_json::Map::new();
            entry.insert(
                "id".to_string(),
                serde_json::json!(format!("question_{}", index + 1)),
            );
            entry.insert(
                "header".to_string(),
                serde_json::json!(format!("Q{}", index + 1)),
            );
            entry.insert("question".to_string(), serde_json::json!(question));
            if let Some(focus_area) = focus_area {
                entry.insert("focus_area".to_string(), serde_json::json!(focus_area));
            }
            if !analysis_hints.is_empty() {
                entry.insert(
                    "analysis_hints".to_string(),
                    serde_json::json!(analysis_hints),
                );
            }
            serde_json::Value::Object(entry)
        })
        .collect::<Vec<_>>();

    Some(serde_json::json!({ "questions": payload }))
}

pub(crate) fn extract_interview_questions(text: &str) -> Vec<String> {
    let mut questions = Vec::new();

    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        if let Some(question) = parse_numbered_question(trimmed) {
            questions.push(question);
            continue;
        }
        if let Some(question) = parse_bullet_question(trimmed) {
            questions.push(question);
        }
    }

    if questions.is_empty() {
        let trimmed = text.trim();
        let normalized = normalize_question_line(trimmed);
        if !normalized.is_empty() && normalized.contains('?') && normalized.len() <= 200 {
            questions.push(normalized);
        }
    }

    questions.truncate(3);
    questions
}

fn parse_numbered_question(line: &str) -> Option<String> {
    let mut digits_len = 0usize;
    for ch in line.chars() {
        if ch.is_ascii_digit() {
            digits_len += ch.len_utf8();
        } else {
            break;
        }
    }
    if digits_len == 0 {
        return None;
    }

    let rest = line[digits_len..].trim_start();
    let mut chars = rest.chars();
    let punct = chars.next()?;
    if punct != '.' && punct != ')' {
        return None;
    }
    let remainder = chars.as_str().trim_start();
    let normalized = normalize_question_line(remainder);
    if normalized.contains('?') {
        Some(normalized)
    } else {
        None
    }
}

fn parse_bullet_question(line: &str) -> Option<String> {
    for prefix in ["- ", "* ", "• "] {
        if let Some(stripped) = line.strip_prefix(prefix) {
            let candidate = normalize_question_line(stripped.trim());
            if candidate.contains('?') {
                return Some(candidate);
            }
        }
    }
    None
}

fn normalize_question_line(line: &str) -> String {
    let mut current = line.trim();

    if let Some(stripped) = current.strip_prefix('>') {
        current = stripped.trim_start();
    }

    let mut changed = true;
    while changed {
        changed = false;
        if let Some(stripped) = strip_wrapping(current, "**", "**") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "__", "__") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "`", "`") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "*", "*") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "_", "_") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "\"", "\"") {
            current = stripped;
            changed = true;
        } else if let Some(stripped) = strip_wrapping(current, "'", "'") {
            current = stripped;
            changed = true;
        }
    }

    current.trim().to_string()
}

fn synthesize_alignment_question(text: &str) -> Option<String> {
    let lower = text.to_lowercase();
    if !contains_any(
        &lower,
        &[
            "need clarification",
            "need your input",
            "before finalizing",
            "before finalising",
            "open questions",
            "for alignment",
            "key decisions",
            "decision points",
        ],
    ) {
        return None;
    }

    if contains_any(
        &lower,
        &["system prompt", "prompt architecture", "prompt variants"],
    ) {
        return Some(
            "Which system prompt improvement area should we prioritize first?".to_string(),
        );
    }

    if lower.contains("plan mode") {
        return Some("Which plan mode improvement area should we prioritize first?".to_string());
    }

    Some("Which improvement area should we prioritize first?".to_string())
}

fn infer_focus_area(text: &str) -> Option<&'static str> {
    let lower = text.to_lowercase();
    if contains_any(
        &lower,
        &["system prompt", "prompt architecture", "prompt variants"],
    ) {
        return Some("system_prompt");
    }
    if lower.contains("plan mode") {
        return Some("plan_mode");
    }
    if contains_any(&lower, &["verification", "test coverage", "validation"]) {
        return Some("verification");
    }
    None
}

fn extract_analysis_hints(text: &str) -> Vec<String> {
    let mut hints = Vec::new();

    for line in text.lines() {
        if hints.len() >= 8 {
            break;
        }

        let normalized = normalize_hint_line(line);
        if normalized.len() < 12 || normalized.contains('?') {
            continue;
        }

        let lower = normalized.to_lowercase();
        if !contains_any(
            &lower,
            &[
                "redundan",
                "overlap",
                "missing",
                "failure",
                "timeout",
                "fallback",
                "loop",
                "optimiz",
                "token",
                "prompt",
                "harness",
                "doc",
                "verification",
                "test",
                "quality",
                "risk",
                "constraint",
                "circular",
            ],
        ) {
            continue;
        }

        if hints
            .iter()
            .any(|existing: &String| existing.eq_ignore_ascii_case(&normalized))
        {
            continue;
        }

        hints.push(normalized);
    }

    hints
}

fn normalize_hint_line(line: &str) -> String {
    let mut current = line.trim();

    for prefix in ["- ", "* ", "• "] {
        if let Some(stripped) = current.strip_prefix(prefix) {
            current = stripped.trim_start();
            break;
        }
    }

    let mut digit_len = 0usize;
    for ch in current.chars() {
        if ch.is_ascii_digit() {
            digit_len += ch.len_utf8();
        } else {
            break;
        }
    }
    if digit_len > 0 {
        let rest = current[digit_len..].trim_start();
        if let Some(stripped) = rest.strip_prefix('.') {
            current = stripped.trim_start();
        } else if let Some(stripped) = rest.strip_prefix(')') {
            current = stripped.trim_start();
        }
    }

    normalize_question_line(current)
}

fn contains_any(text: &str, needles: &[&str]) -> bool {
    needles.iter().any(|needle| text.contains(needle))
}

fn strip_wrapping<'a>(line: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
    if line.len() <= prefix.len() + suffix.len() {
        return None;
    }
    if !line.starts_with(prefix) || !line.ends_with(suffix) {
        return None;
    }
    Some(line[prefix.len()..line.len() - suffix.len()].trim())
}

#[cfg(test)]
mod tests {
    use super::*;
    use vtcode_core::config::constants::tools;
    use vtcode_core::llm::provider::{FinishReason, LLMResponse};

    #[test]
    fn prepare_tool_calls_reuses_parsed_arguments_and_metadata() {
        let tool_calls = vec![
            vtcode_core::llm::provider::ToolCall::function(
                "call_search".to_string(),
                tools::UNIFIED_SEARCH.to_string(),
                r#"{"action":"grep","pattern":"TurnProcessingResult"}"#.to_string(),
            ),
            vtcode_core::llm::provider::ToolCall::function(
                "call_exec".to_string(),
                tools::UNIFIED_EXEC.to_string(),
                r#"{"action":"run","command":["cargo","check"]}"#.to_string(),
            ),
        ];

        let prepared = prepare_tool_calls(tool_calls);

        assert_eq!(prepared.len(), 2);
        assert_eq!(prepared[0].call_id(), "call_search");
        assert_eq!(prepared[0].tool_name(), tools::UNIFIED_SEARCH);
        assert_eq!(
            prepared[0].args(),
            Some(&serde_json::json!({"action":"grep","pattern":"TurnProcessingResult"}))
        );
        assert!(prepared[0].is_parallel_safe());
        assert!(!prepared[0].is_command_execution());

        assert_eq!(prepared[1].call_id(), "call_exec");
        assert_eq!(prepared[1].tool_name(), tools::UNIFIED_EXEC);
        assert_eq!(
            prepared[1].args(),
            Some(&serde_json::json!({"action":"run","command":["cargo","check"]}))
        );
        assert!(!prepared[1].is_parallel_safe());
        assert!(prepared[1].is_command_execution());
    }

    #[test]
    fn prepare_tool_calls_records_invalid_json_without_reparsing() {
        let tool_calls = vec![vtcode_core::llm::provider::ToolCall::function(
            "call_invalid".to_string(),
            tools::UNIFIED_SEARCH.to_string(),
            "{not-json}".to_string(),
        )];

        let prepared = prepare_tool_calls(tool_calls);

        assert_eq!(prepared.len(), 1);
        assert_eq!(prepared[0].call_id(), "call_invalid");
        assert_eq!(prepared[0].tool_name(), tools::UNIFIED_SEARCH);
        assert!(prepared[0].args().is_none());
        assert!(prepared[0].args_error().is_some());
        assert!(!prepared[0].is_parallel_safe());
        assert!(!prepared[0].is_command_execution());
    }

    #[test]
    fn prepare_tool_calls_keeps_custom_tool_payload_as_raw_string() {
        let tool_calls = vec![vtcode_core::llm::provider::ToolCall::custom(
            "call_patch".to_string(),
            tools::APPLY_PATCH.to_string(),
            "*** Begin Patch\n*** End Patch\n".to_string(),
        )];

        let prepared = prepare_tool_calls(tool_calls);

        assert_eq!(prepared.len(), 1);
        assert_eq!(prepared[0].call_id(), "call_patch");
        assert_eq!(prepared[0].tool_name(), tools::APPLY_PATCH);
        assert_eq!(
            prepared[0].args(),
            Some(&serde_json::json!("*** Begin Patch\n*** End Patch\n"))
        );
        assert!(prepared[0].args_error().is_none());
        assert!(!prepared[0].is_parallel_safe());
        assert!(!prepared[0].is_command_execution());
    }

    #[test]
    fn extract_interview_questions_from_numbered_lines() {
        let text = "1. First question?\n2) Second question?\n3. Third question?";
        let questions = extract_interview_questions(text);
        assert_eq!(questions.len(), 3);
        assert_eq!(questions[0], "First question?");
        assert_eq!(questions[1], "Second question?");
        assert_eq!(questions[2], "Third question?");
    }

    #[test]
    fn extract_interview_questions_from_bullets() {
        let text = "- Should we do X?\n- Should we do Y?";
        let questions = extract_interview_questions(text);
        assert_eq!(questions.len(), 2);
        assert_eq!(questions[0], "Should we do X?");
    }

    #[test]
    fn process_llm_response_turns_questions_into_tool_call() {
        let response = LLMResponse {
            content: Some("1. First question?\n2. Second question?".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            false,
            true,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::ToolCalls { tool_calls, .. } => {
                assert_eq!(tool_calls.len(), 1);
            }
            _ => panic!("Expected tool calls"),
        }
    }

    #[tokio::test]
    async fn process_llm_response_rejects_textual_unified_exec_without_command() {
        let temp = tempfile::tempdir().expect("temp workspace");
        let tool_registry = vtcode_core::tools::ToolRegistry::new(temp.path().to_path_buf()).await;
        let response = LLMResponse {
            content: Some("run()".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            7,
            false,
            false,
            true,
            true,
            None,
            Some(&tool_registry),
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse { text, .. } => {
                assert_eq!(text, "run()");
            }
            _ => panic!("Expected textual invalid unified_exec to stay a text response"),
        }
    }

    #[test]
    fn process_llm_response_skips_questions_when_interview_not_ready() {
        let response = LLMResponse {
            content: Some("1. First question?\n2. Second question?".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            false,
            false,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse { text, .. } => {
                assert!(text.contains("First question"));
            }
            _ => panic!("Expected text response without tool calls"),
        }
    }

    #[test]
    fn process_llm_response_keeps_plan_visible_in_plan_mode() {
        let response = LLMResponse {
            content: Some("Intro\n<proposed_plan>\n- Step 1\n</proposed_plan>\nOutro".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            true,
            false,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse {
                text,
                proposed_plan,
                ..
            } => {
                assert_eq!(text, "Intro\n\n- Step 1\n\nOutro");
                assert!(!text.contains("<proposed_plan>"));
                assert_eq!(proposed_plan.as_deref(), Some("- Step 1"));
            }
            _ => panic!("Expected text response with visible proposed plan"),
        }
    }

    #[test]
    fn process_llm_response_extracts_plan_block_in_plan_mode() {
        let response = LLMResponse {
            content: Some("Intro\n<plan>\n- Step 1\n</plan>\nOutro".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            true,
            false,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse {
                text,
                proposed_plan,
                ..
            } => {
                assert_eq!(proposed_plan.as_deref(), Some("- Step 1"));
                assert!(!text.contains("<plan>"));
                assert!(!text.contains("</plan>"));
            }
            _ => panic!("Expected text response with extracted <plan> block"),
        }
    }

    #[test]
    fn process_llm_response_extracts_sparse_proposed_plan_blocks() {
        let response = LLMResponse {
            content: Some(
                r#"Intro
Next open decision: none.
<proposed_plan>
# Apply Slate-Style Prompting

## Summary
Keep the default runtime prompt sparse and consistent with Plan Mode.

## Implementation Steps
1. Update prompt constants -> files: [vtcode-core/src/prompts/system.rs] -> verify: [cargo check]
2. Update plan scaffold -> files: [vtcode-core/src/tools/handlers/plan_mode.rs] -> verify: [cargo test -p vtcode-core test_enter_plan_mode -- --nocapture]
3. Update parser tests -> files: [src/agent/runloop/unified/turn/turn_processing/response_processing.rs] -> verify: [cargo test -p vtcode process_llm_response_extracts_sparse_proposed_plan_blocks -- --nocapture]

## Test Cases and Validation
1. Build and lint: [project build and lint command(s) based on detected toolchain]
2. Tests: [project test command(s) based on detected toolchain]
3. Targeted behavior checks: plan-mode transcript extraction

## Assumptions and Defaults
1. `Next open decision` remains the only explicit reopen marker.
2. The proposed plan stays decision-complete without rigid extra sections.
</proposed_plan>
Outro"#
                    .to_string(),
            ),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            true,
            false,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse {
                text,
                proposed_plan,
                ..
            } => {
                let plan = proposed_plan.expect("expected extracted proposed plan");
                for required_section in [
                    "## Summary",
                    "## Implementation Steps",
                    "## Test Cases and Validation",
                    "## Assumptions and Defaults",
                ] {
                    assert!(
                        plan.contains(required_section),
                        "proposed_plan missing required section: {required_section}"
                    );
                }
                assert!(!plan.contains("## Scope Locked"));
                assert!(!plan.contains("## Public API / Interface Changes"));
                assert!(!text.contains("<proposed_plan>"));
                assert!(text.contains("Intro"));
                assert!(text.contains("Outro"));
            }
            _ => panic!("Expected text response with extracted proposed plan"),
        }
    }

    #[test]
    fn extract_interview_questions_strips_markdown_wrapping() {
        let text = "**How should we proceed?**";
        let questions = extract_interview_questions(text);
        assert_eq!(questions, vec!["How should we proceed?".to_string()]);
    }

    #[test]
    fn extract_interview_questions_handles_bold_bullets() {
        let text = "- **Should we do X?**";
        let questions = extract_interview_questions(text);
        assert_eq!(questions, vec!["Should we do X?".to_string()]);
    }

    #[test]
    fn build_interview_args_synthesizes_alignment_question_with_hints() {
        let text = r#"
I've analyzed the current system prompt architecture.
The plan is drafted. I need clarification on 3 key decisions before finalizing the implementation approach.
Key findings:
• Redundancy exists between prompt variants (tool guidance, bias for action warnings)
• Missing explicit guidance for common failure patterns (patch failures, circular deps)
• Harness integration is good but could be strengthened with more specific doc refs
Open questions for alignment:
"#;

        let args =
            build_interview_args_from_text(text).expect("expected synthesized interview args");
        let questions = args["questions"]
            .as_array()
            .expect("questions should be an array");
        assert_eq!(questions.len(), 1);

        let first = &questions[0];
        let question_text = first["question"]
            .as_str()
            .expect("question should be a string");
        assert!(question_text.contains("prioritize"));
        assert_eq!(first["focus_area"].as_str(), Some("system_prompt"));

        let hints = first["analysis_hints"]
            .as_array()
            .expect("analysis_hints should exist");
        assert!(!hints.is_empty(), "expected extracted hints");
    }

    #[test]
    fn process_llm_response_turns_alignment_request_into_tool_call() {
        let response = LLMResponse {
            content: Some(
                "Need clarification before finalizing.\nOpen questions for alignment:".to_string(),
            ),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            1,
            true,
            true,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::ToolCalls { tool_calls, .. } => {
                let name = tool_calls
                    .first()
                    .map(|call| call.tool_name())
                    .expect("function name expected");
                assert_eq!(name, tools::REQUEST_USER_INPUT);
            }
            _ => panic!("Expected tool calls"),
        }
    }

    #[test]
    fn process_llm_response_preserves_reasoning_details_for_tool_calls() {
        let response = LLMResponse {
            content: Some("".to_string()),
            tool_calls: Some(vec![vtcode_core::llm::provider::ToolCall::function(
                "call_1".to_string(),
                "unified_search".to_string(),
                r#"{"action":"grep","pattern":"x"}"#.to_string(),
            )]),
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::ToolCalls,
            reasoning: None,
            reasoning_details: Some(vec![
                r#"{"type":"reasoning_content","text":"trace"}"#.to_string(),
            ]),
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            false,
            false,
            true,
            true,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::ToolCalls {
                tool_calls,
                reasoning_details,
                ..
            } => {
                assert_eq!(
                    reasoning_details,
                    Some(vec![
                        r#"{"type":"reasoning_content","text":"trace"}"#.to_string()
                    ])
                );
                assert_eq!(tool_calls.len(), 1);
                assert_eq!(tool_calls[0].call_id(), "call_1");
                assert!(tool_calls[0].is_parallel_safe());
                assert!(!tool_calls[0].is_command_execution());
            }
            _ => panic!("Expected tool calls"),
        }
    }

    #[test]
    fn process_llm_response_keeps_textual_tool_request_as_text_when_tool_calls_disabled() {
        let response = LLMResponse {
            content: Some("unified_search({\"action\":\"list\",\"path\":\"src\"})".to_string()),
            tool_calls: None,
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::Stop,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            false,
            false,
            true,
            false,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse { text, .. } => {
                assert_eq!(
                    text,
                    "unified_search({\"action\":\"list\",\"path\":\"src\"})"
                );
            }
            _ => panic!("Expected text response when tool calls are disabled"),
        }
    }

    #[test]
    fn process_llm_response_ignores_structured_tool_calls_when_tool_calls_disabled() {
        let response = LLMResponse {
            content: Some("Final synthesis only.".to_string()),
            tool_calls: Some(vec![vtcode_core::llm::provider::ToolCall::function(
                "call_1".to_string(),
                "unified_search".to_string(),
                r#"{"action":"list","path":"src"}"#.to_string(),
            )]),
            model: "test".to_string(),
            usage: None,
            finish_reason: FinishReason::ToolCalls,
            reasoning: None,
            reasoning_details: None,
            tool_references: Vec::new(),
            compaction: None,
            request_id: None,
            organization_id: None,
        };

        let mut renderer = AnsiRenderer::stdout();
        let result = process_llm_response(
            &response,
            &mut renderer,
            0,
            false,
            false,
            true,
            false,
            None,
            None,
        )
        .expect("processing should succeed");

        match result {
            TurnProcessingResult::TextResponse { text, .. } => {
                assert_eq!(text, "Final synthesis only.");
            }
            _ => panic!("Expected text response when tool calls are disabled"),
        }
    }
}
