//! AutoForge: Automated Apply-Test-Verify Workflow Engine
//!
//! AutoForge orchestrates the full lifecycle of automated code changes:
//!
//! 1. **Auto-Apply** -- writes generated code to the filesystem based on task
//!    definitions (create, modify, delete operations).
//! 2. **Auto-Test** -- runs the project's test suite and collects pass/fail
//!    counts by parsing runner output.
//! 3. **Auto-Verify** -- performs static analysis heuristics on changed files
//!    to detect hardcoded secrets, TODOs, overly long functions, etc.
//! 4. **Report** -- assembles a comprehensive `AutoForgeReport` with status
//!    determination.
//! 5. **Decision** -- returns the report so a human (or upstream agent) can
//!    decide to Accept, Review, Reject, or Iterate.
//!
//! Named after the concept of an automated forge -- where raw materials
//! (tasks, diffs, generated code) are hammered into shape without manual
//! intervention.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{bail, Context, Result};
use chrono::Utc;
use regex::Regex;
use tracing::{debug, error, info, warn};

use crate::db::SqliteDb;
use crate::types::*;

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/// Maximum number of lines a single function should span before triggering
/// a complexity warning.
const MAX_FUNCTION_LENGTH: usize = 50;

/// Maximum file size (in bytes) before we flag it as overly large.
const MAX_FILE_SIZE_BYTES: u64 = 100_000;

/// Patterns that indicate hardcoded secrets. Each pattern is matched
/// case-insensitively against every line of every changed file.
const SECRET_PATTERNS: &[&str] = &[
    r#"(?i)password\s*=\s*["'][^"']+["']"#,
    r#"(?i)api_key\s*=\s*["'][^"']+["']"#,
    r#"(?i)secret\s*=\s*["'][^"']+["']"#,
    r#"(?i)api_secret\s*=\s*["'][^"']+["']"#,
    r#"(?i)auth_token\s*=\s*["'][^"']+["']"#,
    r#"(?i)private_key\s*=\s*["'][^"']+["']"#,
];

/// Patterns indicating incomplete work (TODO, FIXME, HACK, XXX).
const TODO_PATTERNS: &[&str] = &[
    r"(?i)\bTODO\b",
    r"(?i)\bFIXME\b",
    r"(?i)\bHACK\b",
    r"(?i)\bXXX\b",
];

// ---------------------------------------------------------------------------
// AutoForge
// ---------------------------------------------------------------------------

/// The AutoForge workflow engine.
///
/// Holds a reference to the SQLite database (for loading scrolls/tasks) and
/// the workspace root directory (for filesystem operations).
pub struct AutoForge {
    /// SQLite database for scroll and task persistence.
    sqlite: Arc<SqliteDb>,

    /// Root directory of the workspace where files are applied.
    workspace_root: PathBuf,

    /// Optional Trinity agent for test generation during AutoForge.
    trinity: Option<Arc<crate::agents::trinity::Trinity>>,

    /// Optional LLM provider for test command detection fallback.
    llm: Option<Arc<dyn crate::types::LlmProvider>>,

    /// Optional Legolas agent for enhanced post-apply code review.
    legolas: Option<Arc<crate::agents::legolas::Legolas>>,

    /// Optional Morpheus agent for pre-apply validation (types + breaking changes).
    morpheus: Option<Arc<crate::agents::morpheus::Morpheus>>,
}

impl AutoForge {
    /// Create a new AutoForge instance.
    pub fn new(sqlite: Arc<SqliteDb>, workspace_root: PathBuf) -> Self {
        info!(workspace = %workspace_root.display(), "AutoForge initialized");
        Self {
            sqlite,
            workspace_root,
            trinity: None,
            llm: None,
            legolas: None,
            morpheus: None,
        }
    }

    /// Attach a Trinity agent for generating tests during AutoForge workflows.
    pub fn with_trinity(mut self, trinity: Arc<crate::agents::trinity::Trinity>) -> Self {
        self.trinity = Some(trinity);
        self
    }

    /// Attach an LLM provider for test command detection fallback.
    pub fn with_llm(mut self, llm: Arc<dyn crate::types::LlmProvider>) -> Self {
        self.llm = Some(llm);
        self
    }

    /// Attach a Legolas agent for enhanced post-apply code review.
    pub fn with_legolas(mut self, legolas: Arc<crate::agents::legolas::Legolas>) -> Self {
        self.legolas = Some(legolas);
        self
    }

    /// Attach a Morpheus agent for pre-apply validation.
    pub fn with_morpheus(mut self, morpheus: Arc<crate::agents::morpheus::Morpheus>) -> Self {
        self.morpheus = Some(morpheus);
        self
    }

    // -----------------------------------------------------------------------
    // Main entry point
    // -----------------------------------------------------------------------

    /// Execute the full AutoForge workflow for a scroll.
    ///
    /// Loads the scroll and its tasks from the database, then runs each
    /// enabled phase (apply, test, verify) according to the scroll's
    /// `AutoForgeConfig`. Finally, assembles and returns an `AutoForgeReport`.
    pub async fn execute(&self, scroll_id: &str) -> Result<AutoForgeReport> {
        info!(scroll_id, "Starting AutoForge workflow");

        // 1. Load the scroll and its configuration.
        let scroll = self
            .sqlite
            .get_scroll(scroll_id)
            .await
            .context("Failed to load scroll for AutoForge")?;

        let config = scroll
            .metadata
            .auto_forge_config
            .clone()
            .unwrap_or_default();

        // 2. Load tasks for this scroll.
        let tasks = self
            .sqlite
            .get_tasks_for_scroll(scroll_id)
            .await
            .context("Failed to load tasks for AutoForge")?;

        if tasks.is_empty() {
            warn!(scroll_id, "No tasks found for AutoForge execution");
            let empty_test = TestResults {
                total: 0,
                passed: 0,
                failed: 0,
                skipped: 0,
                output: String::new(),
            };
            let empty_analysis = CodeAnalysis {
                issues: vec![],
                metrics: AnalysisMetrics {
                    complexity: 0.0,
                    test_coverage: 0.0,
                    dependency_count: 0,
                },
            };
            return Ok(self.generate_report(scroll_id, empty_test, empty_analysis));
        }

        // Mark scroll as in-progress.
        if let Err(e) = self
            .sqlite
            .update_scroll_status(scroll_id, &ScrollStatus::InProgress)
            .await
        {
            warn!(scroll_id, error = %e, "Failed to update scroll status to InProgress");
        }

        // NOTE: Morpheus pre-apply validation is handled by the MCP handler
        // (ExecuteScrollHandler) BEFORE Aragorn/AutoForge execute. This avoids
        // redundant double-validation and ensures errors block before any code
        // is applied to disk.

        // 3. Phase 1: Auto-Apply
        let changed_files = if config.auto_apply {
            info!(scroll_id, task_count = tasks.len(), "Phase 1: Auto-Apply");
            let apply_activity_id = format!("act-autoforge-apply-{}-{}", scroll_id, Utc::now().timestamp_millis());
            let _ = self.sqlite.record_agent_activity(&AgentActivity {
                id: apply_activity_id.clone(),
                scroll_id: scroll_id.to_string(),
                task_id: String::new(),
                agent_name: "AutoForge".into(),
                action: "auto_apply".into(),
                detail: format!("{} tasks", tasks.len()),
                started_at: Utc::now(),
                completed_at: None,
                status: AgentActivityStatus::Running,
            }).await;
            match self.auto_apply(&tasks).await {
                Ok(files) => {
                    info!(
                        scroll_id,
                        changed = files.len(),
                        "Auto-Apply completed"
                    );
                    let _ = self.sqlite.complete_agent_activity(
                        &apply_activity_id,
                        &AgentActivityStatus::Completed,
                        &format!("{} files changed", files.len()),
                    ).await;
                    files
                }
                Err(e) => {
                    let _ = self.sqlite.complete_agent_activity(
                        &apply_activity_id,
                        &AgentActivityStatus::Failed,
                        &format!("{}", e),
                    ).await;
                    error!(scroll_id, error = %e, "Auto-Apply failed");
                    let failed_test = TestResults {
                        total: 0,
                        passed: 0,
                        failed: 0,
                        skipped: 0,
                        output: format!("Auto-Apply failed: {}", e),
                    };
                    let failed_analysis = CodeAnalysis {
                        issues: vec![AnalysisIssue {
                            file_path: String::new(),
                            line: 0,
                            severity: Severity::Error,
                            message: format!("Auto-Apply phase failed: {}", e),
                        }],
                        metrics: AnalysisMetrics {
                            complexity: 0.0,
                            test_coverage: 0.0,
                            dependency_count: 0,
                        },
                    };
                    return Ok(self.generate_report(scroll_id, failed_test, failed_analysis));
                }
            }
        } else {
            info!(scroll_id, "Auto-Apply disabled, skipping");
            vec![]
        };

        // 3b. Phase 1b: Trinity Test Generation (if available)
        //     Re-fetch tasks from DB so Trinity sees Gimli's generated code
        //     (the local `tasks` vec was loaded before Aragorn/Gimli executed).
        let mut test_specs: Vec<serde_json::Value> = Vec::new();
        if let Some(ref trinity) = self.trinity {
            let tasks = self
                .sqlite
                .get_tasks_for_scroll(scroll_id)
                .await
                .context("Failed to reload tasks for Trinity")?;
            info!(scroll_id, "Phase 1b: Trinity test generation");

            // Collect tasks that have generated code for batch processing.
            let tasks_with_code: Vec<(&crate::types::Task, &str)> = tasks
                .iter()
                .filter_map(|t| {
                    t.generated_code
                        .as_deref()
                        .map(|code| (t, code))
                })
                .collect();

            if tasks_with_code.len() > 1 {
                // Batch path: generate tests for all tasks in a single LLM call.
                let batch_activity_id = format!("act-trinity-batch-{}-{}", scroll_id, Utc::now().timestamp_millis());
                let _ = self.sqlite.record_agent_activity(&AgentActivity {
                    id: batch_activity_id.clone(),
                    scroll_id: scroll_id.to_string(),
                    task_id: String::new(),
                    agent_name: "Trinity".into(),
                    action: "generate_tests_batch".into(),
                    detail: format!("{} tasks", tasks_with_code.len()),
                    started_at: Utc::now(),
                    completed_at: None,
                    status: AgentActivityStatus::Running,
                }).await;

                match tokio::time::timeout(
                    std::time::Duration::from_secs(180),
                    trinity.generate_tests_batch(&tasks_with_code),
                ).await {
                    Ok(Ok(results)) => {
                        for (task_id, test_code) in &results {
                            if let Some((task, _)) = tasks_with_code.iter().find(|(t, _)| t.id == *task_id) {
                                let test_path = Self::derive_test_path(&task.target_file);
                                let full_test_path = self.workspace_root.join(&test_path);
                                if let Some(parent) = full_test_path.parent() {
                                    let _ = tokio::fs::create_dir_all(parent).await;
                                }
                                if let Err(e) = tokio::fs::write(&full_test_path, test_code).await {
                                    warn!(task_id = %task_id, error = %e, "Failed to write batch test file");
                                } else {
                                    info!(task_id = %task_id, test_path = %test_path, "Batch test generated");
                                }
                            }
                        }
                        let _ = self.sqlite.complete_agent_activity(
                            &batch_activity_id,
                            &AgentActivityStatus::Completed,
                            &format!("{} tests generated", results.len()),
                        ).await;
                    }
                    Ok(Err(e)) => {
                        warn!(error = %e, "Trinity batch test generation failed (collecting specs for host)");
                        let _ = self.sqlite.complete_agent_activity(
                            &batch_activity_id,
                            &AgentActivityStatus::Failed,
                            &format!("{}", e),
                        ).await;
                        for (task, _code) in &tasks_with_code {
                            test_specs.push(serde_json::json!({
                                "target_file": task.target_file,
                                "test_path": Self::derive_test_path(&task.target_file),
                                "language": crate::agents::trinity::Trinity::detect_language(&task.target_file),
                                "code_under_test": task.generated_code.as_deref().unwrap_or(""),
                                "description": task.description,
                            }));
                        }
                    }
                    Err(_) => {
                        warn!("Trinity batch test generation timed out after 180s");
                        let _ = self.sqlite.complete_agent_activity(
                            &batch_activity_id,
                            &AgentActivityStatus::Failed,
                            "timed out after 180s",
                        ).await;
                        for (task, _code) in &tasks_with_code {
                            test_specs.push(serde_json::json!({
                                "target_file": task.target_file,
                                "test_path": Self::derive_test_path(&task.target_file),
                                "language": crate::agents::trinity::Trinity::detect_language(&task.target_file),
                                "code_under_test": task.generated_code.as_deref().unwrap_or(""),
                                "description": task.description,
                            }));
                        }
                    }
                }
            } else {
                // Single task or no tasks: use individual generation.
                for (task, code) in &tasks_with_code {
                    let trinity_activity_id = format!("act-trinity-{}-{}", task.id, Utc::now().timestamp_millis());
                    let _ = self.sqlite.record_agent_activity(&AgentActivity {
                        id: trinity_activity_id.clone(),
                        scroll_id: scroll_id.to_string(),
                        task_id: task.id.clone(),
                        agent_name: "Trinity".into(),
                        action: "generate_tests".into(),
                        detail: format!("target: {}", task.target_file),
                        started_at: Utc::now(),
                        completed_at: None,
                        status: AgentActivityStatus::Running,
                    }).await;
                    match tokio::time::timeout(
                        std::time::Duration::from_secs(120),
                        trinity.generate_tests(task, code),
                    ).await {
                        Ok(Ok(test_code)) => {
                            let test_path = Self::derive_test_path(&task.target_file);
                            let full_test_path = self.workspace_root.join(&test_path);
                            if let Some(parent) = full_test_path.parent() {
                                let _ = tokio::fs::create_dir_all(parent).await;
                            }
                            if let Err(e) = tokio::fs::write(&full_test_path, &test_code).await {
                                warn!(task_id = %task.id, error = %e, "Failed to write Trinity test file");
                                let _ = self.sqlite.complete_agent_activity(
                                    &trinity_activity_id,
                                    &AgentActivityStatus::Failed,
                                    &format!("write failed: {}", e),
                                ).await;
                            } else {
                                info!(task_id = %task.id, test_path = %test_path, "Trinity test generated");
                                let _ = self.sqlite.complete_agent_activity(
                                    &trinity_activity_id,
                                    &AgentActivityStatus::Completed,
                                    &format!("test written to {}", test_path),
                                ).await;
                            }
                        }
                        Ok(Err(e)) => {
                            warn!(task_id = %task.id, error = %e, "Trinity test generation failed");
                            let _ = self.sqlite.complete_agent_activity(
                                &trinity_activity_id,
                                &AgentActivityStatus::Failed,
                                &format!("{}", e),
                            ).await;
                            test_specs.push(serde_json::json!({
                                "target_file": task.target_file,
                                "test_path": Self::derive_test_path(&task.target_file),
                                "language": crate::agents::trinity::Trinity::detect_language(&task.target_file),
                                "code_under_test": task.generated_code.as_deref().unwrap_or(""),
                                "description": task.description,
                            }));
                        }
                        Err(_) => {
                            warn!(task_id = %task.id, "Trinity test generation timed out after 120s");
                            let _ = self.sqlite.complete_agent_activity(
                                &trinity_activity_id,
                                &AgentActivityStatus::Failed,
                                "timed out after 120s",
                            ).await;
                            test_specs.push(serde_json::json!({
                                "target_file": task.target_file,
                                "test_path": Self::derive_test_path(&task.target_file),
                                "language": crate::agents::trinity::Trinity::detect_language(&task.target_file),
                                "code_under_test": task.generated_code.as_deref().unwrap_or(""),
                                "description": task.description,
                            }));
                        }
                    }
                }
            }
        }

        // 4. Phase 2: Auto-Test
        let test_results = if config.auto_test {
            info!(scroll_id, "Phase 2: Auto-Test");
            let test_activity_id = format!("act-autoforge-test-{}-{}", scroll_id, Utc::now().timestamp_millis());
            let _ = self.sqlite.record_agent_activity(&AgentActivity {
                id: test_activity_id.clone(),
                scroll_id: scroll_id.to_string(),
                task_id: String::new(),
                agent_name: "AutoForge".into(),
                action: "auto_test".into(),
                detail: String::new(),
                started_at: Utc::now(),
                completed_at: None,
                status: AgentActivityStatus::Running,
            }).await;
            match self.auto_test(&config).await {
                Ok(results) => {
                    info!(
                        scroll_id,
                        total = results.total,
                        passed = results.passed,
                        failed = results.failed,
                        "Auto-Test completed"
                    );
                    let _ = self.sqlite.complete_agent_activity(
                        &test_activity_id,
                        &AgentActivityStatus::Completed,
                        &format!("{} passed, {} failed", results.passed, results.failed),
                    ).await;
                    results
                }
                Err(e) => {
                    warn!(scroll_id, error = %e, "Auto-Test failed to run");
                    let _ = self.sqlite.complete_agent_activity(
                        &test_activity_id,
                        &AgentActivityStatus::Failed,
                        &format!("{}", e),
                    ).await;
                    TestResults {
                        total: 0,
                        passed: 0,
                        failed: 0,
                        skipped: 0,
                        output: format!("Test execution failed: {}", e),
                    }
                }
            }
        } else {
            info!(scroll_id, "Auto-Test disabled, skipping");
            TestResults {
                total: 0,
                passed: 0,
                failed: 0,
                skipped: 0,
                output: "Auto-Test disabled".to_string(),
            }
        };

        // 5. Phase 3: Auto-Verify (Legolas-enhanced when available)
        let (analysis, legolas_code_review) = if config.auto_verify {
            if let Some(ref legolas) = self.legolas {
                // Legolas-enhanced verification: per-file code analysis.
                info!(scroll_id, file_count = changed_files.len(), "Phase 3: Legolas code review");
                let verify_activity_id = format!("act-legolas-verify-{}-{}", scroll_id, Utc::now().timestamp_millis());
                let _ = self.sqlite.record_agent_activity(&AgentActivity {
                    id: verify_activity_id.clone(),
                    scroll_id: scroll_id.to_string(),
                    task_id: String::new(),
                    agent_name: "Legolas".into(),
                    action: "code_review".into(),
                    detail: format!("{} files", changed_files.len()),
                    started_at: Utc::now(),
                    completed_at: None,
                    status: AgentActivityStatus::Running,
                }).await;

                let mut per_file_reviews: Vec<CodeAnalysis> = Vec::new();
                let mut all_issues: Vec<AnalysisIssue> = Vec::new();
                let mut total_complexity = 0.0f64;
                let mut total_dep_count = 0usize;

                // Re-fetch tasks to get fresh generated_code
                let fresh_tasks = self.sqlite.get_tasks_for_scroll(scroll_id)
                    .await
                    .unwrap_or_default();

                // Step 1: Concurrent heuristic analysis (no LLM calls).
                let mut handles = Vec::new();
                let mut files_for_llm: Vec<(String, String)> = Vec::new();
                for task in &fresh_tasks {
                    if let Some(code) = task.generated_code.clone() {
                        let legolas = Arc::clone(legolas);
                        let file_path = task.target_file.clone();
                        files_for_llm.push((file_path.clone(), code.clone()));
                        handles.push(tokio::spawn(async move {
                            let result = tokio::time::timeout(
                                std::time::Duration::from_secs(60),
                                legolas.analyze_file_heuristic(&file_path, &code),
                            ).await;
                            (file_path, result)
                        }));
                    }
                }
                for handle in handles {
                    match handle.await {
                        Ok((_, Ok(Ok(file_analysis)))) => {
                            all_issues.extend(file_analysis.issues.clone());
                            total_complexity += file_analysis.metrics.complexity;
                            total_dep_count += file_analysis.metrics.dependency_count;
                            per_file_reviews.push(file_analysis);
                        }
                        Ok((file_path, Ok(Err(e)))) => {
                            warn!(file = %file_path, error = %e, "Legolas analysis failed for file");
                        }
                        Ok((file_path, Err(_))) => {
                            warn!(file = %file_path, "Legolas analysis timed out after 60s");
                        }
                        Err(e) => {
                            warn!(error = %e, "Legolas analysis task panicked");
                        }
                    }
                }

                // Step 2: Single batch LLM code review (1 sampling call).
                if !files_for_llm.is_empty() {
                    let batch_refs: Vec<(&str, &str)> = files_for_llm
                        .iter()
                        .map(|(p, c)| (p.as_str(), c.as_str()))
                        .collect();
                    match legolas.review_code_batch(&batch_refs).await {
                        Ok(batch_results) => {
                            for (file_path, llm_issues) in batch_results {
                                if !llm_issues.is_empty() {
                                    info!(
                                        file = %file_path,
                                        llm_issues = llm_issues.len(),
                                        "Legolas: LLM review found issues"
                                    );
                                    all_issues.extend(llm_issues);
                                }
                            }
                        }
                        Err(e) => {
                            warn!(error = %e, "Legolas batch LLM review failed, heuristic results only");
                        }
                    }
                }

                let aggregate = CodeAnalysis {
                    issues: all_issues,
                    metrics: AnalysisMetrics {
                        complexity: if fresh_tasks.is_empty() { 0.0 } else { total_complexity / fresh_tasks.len() as f64 },
                        test_coverage: 0.0,
                        dependency_count: total_dep_count,
                    },
                };

                let _ = self.sqlite.complete_agent_activity(
                    &verify_activity_id,
                    &AgentActivityStatus::Completed,
                    &format!("{} issues across {} files", aggregate.issues.len(), per_file_reviews.len()),
                ).await;

                info!(
                    scroll_id,
                    issues = aggregate.issues.len(),
                    files_reviewed = per_file_reviews.len(),
                    "Legolas code review completed"
                );

                (aggregate, per_file_reviews)
            } else {
                // Fallback to basic auto_verify
                info!(scroll_id, file_count = changed_files.len(), "Phase 3: Auto-Verify (basic)");
                let verify_activity_id = format!("act-autoforge-verify-{}-{}", scroll_id, Utc::now().timestamp_millis());
                let _ = self.sqlite.record_agent_activity(&AgentActivity {
                    id: verify_activity_id.clone(),
                    scroll_id: scroll_id.to_string(),
                    task_id: String::new(),
                    agent_name: "AutoForge".into(),
                    action: "auto_verify".into(),
                    detail: format!("{} files", changed_files.len()),
                    started_at: Utc::now(),
                    completed_at: None,
                    status: AgentActivityStatus::Running,
                }).await;
                match self.auto_verify(&changed_files).await {
                    Ok(a) => {
                        let _ = self.sqlite.complete_agent_activity(
                            &verify_activity_id,
                            &AgentActivityStatus::Completed,
                            &format!("{} issues, complexity {:.1}", a.issues.len(), a.metrics.complexity),
                        ).await;
                        (a, vec![])
                    }
                    Err(e) => {
                        warn!(scroll_id, error = %e, "Auto-Verify failed");
                        let _ = self.sqlite.complete_agent_activity(
                            &verify_activity_id,
                            &AgentActivityStatus::Failed,
                            &format!("{}", e),
                        ).await;
                        (CodeAnalysis {
                            issues: vec![AnalysisIssue {
                                file_path: String::new(),
                                line: 0,
                                severity: Severity::Warning,
                                message: format!("Verification failed: {}", e),
                            }],
                            metrics: AnalysisMetrics {
                                complexity: 0.0,
                                test_coverage: 0.0,
                                dependency_count: 0,
                            },
                        }, vec![])
                    }
                }
            }
        } else {
            info!(scroll_id, "Auto-Verify disabled, skipping");
            (CodeAnalysis {
                issues: vec![],
                metrics: AnalysisMetrics {
                    complexity: 0.0,
                    test_coverage: 0.0,
                    dependency_count: 0,
                },
            }, vec![])
        };

        // 6. Phase 4: Generate report.
        let report = self.generate_report_full(scroll_id, test_results, analysis, test_specs, legolas_code_review);

        info!(
            scroll_id,
            report_id = report.id.as_str(),
            status = ?report.status,
            "AutoForge workflow complete"
        );

        // Update scroll status based on outcome.
        let final_status = match report.status {
            AutoForgeStatus::AllPassed => ScrollStatus::Completed,
            AutoForgeStatus::Failed => ScrollStatus::Failed,
            _ => ScrollStatus::InProgress,
        };

        if let Err(e) = self
            .sqlite
            .update_scroll_status(scroll_id, &final_status)
            .await
        {
            warn!(scroll_id, error = %e, "Failed to update final scroll status");
        }

        Ok(report)
    }

    /// Execute the parallel task approval workflow.
    ///
    /// This is a consolidated version of `execute` designed for batch approval:
    /// 1. Execute ALL tasks in parallel
    /// 2. Run test suite once
    /// 3. Analyze all changed files
    /// 4. Return a `BatchApprovalReport` for ONE user decision
    pub async fn batch_execute(
        &self,
        scroll_id: &str,
    ) -> Result<crate::types::BatchApprovalReport> {
        info!(scroll_id, "Starting batch approval workflow");

        let scroll = self.sqlite.get_scroll(scroll_id).await?;
        let config = scroll
            .metadata
            .auto_forge_config
            .clone()
            .unwrap_or_default();
        let tasks = self.sqlite.get_tasks_for_scroll(scroll_id).await?;

        if let Err(e) = self
            .sqlite
            .update_scroll_status(scroll_id, &ScrollStatus::InProgress)
            .await
        {
            warn!(scroll_id, error = %e, "Failed to update scroll status");
        }

        // Phase 0.5: Morpheus pre-apply validation (blocking, 30s timeout).
        if let Some(ref morpheus) = self.morpheus {
            let morpheus_result = tokio::time::timeout(
                std::time::Duration::from_secs(30),
                morpheus.validate_scroll(scroll_id),
            ).await;
            match &morpheus_result {
                Err(_) => warn!(scroll_id, "Morpheus validation timed out after 30s in batch_execute, proceeding"),
                Ok(Err(e)) => warn!(scroll_id, error = %e, "Morpheus validation failed in batch_execute, proceeding"),
                Ok(Ok(_)) => {}
            }
            if let Ok(Ok(report)) = morpheus_result {
                let errors: Vec<_> = report.type_errors.iter()
                    .chain(report.breaking_changes.iter())
                    .filter(|r| !r.passed && r.severity == Severity::Error)
                    .collect();

                if !errors.is_empty() {
                    warn!(
                        scroll_id,
                        error_count = errors.len(),
                        "Morpheus pre-apply validation found errors in batch_execute"
                    );
                    let issues: Vec<AnalysisIssue> = errors.iter().map(|e| {
                        AnalysisIssue {
                            file_path: e.constraint_id.clone(),
                            line: 0,
                            severity: Severity::Error,
                            message: e.message.clone(),
                        }
                    }).collect();
                    let failed_analysis = CodeAnalysis {
                        issues,
                        metrics: AnalysisMetrics {
                            complexity: 0.0,
                            test_coverage: 0.0,
                            dependency_count: 0,
                        },
                    };
                    let task_summaries: Vec<crate::types::TaskApprovalSummary> = tasks
                        .iter()
                        .map(|t| crate::types::TaskApprovalSummary {
                            task_id: t.id.clone(),
                            target_file: t.target_file.clone(),
                            task_type: t.task_type.clone(),
                            status: t.status.clone(),
                            lines_changed: 0,
                            has_tests: t.task_type == TaskType::Test,
                        })
                        .collect();
                    return Ok(crate::types::BatchApprovalReport {
                        scroll_id: scroll_id.to_string(),
                        task_summaries,
                        test_results: TestResults {
                            total: 0, passed: 0, failed: 0, skipped: 0,
                            output: "Blocked by Morpheus validation errors".into(),
                        },
                        analysis: failed_analysis,
                        overall_status: AutoForgeStatus::Failed,
                        files_changed: vec![],
                        total_lines_added: 0,
                        total_lines_removed: 0,
                    });
                }
            }
        }

        // Phase 1: Apply all tasks (parallel where possible).
        let changed_files = if config.auto_apply {
            match self.auto_apply(&tasks).await {
                Ok(files) => files,
                Err(e) => {
                    warn!(scroll_id, error = %e, "Auto-Apply failed in batch_execute");
                    let task_summaries: Vec<crate::types::TaskApprovalSummary> = tasks
                        .iter()
                        .map(|t| crate::types::TaskApprovalSummary {
                            task_id: t.id.clone(),
                            target_file: t.target_file.clone(),
                            task_type: t.task_type.clone(),
                            status: t.status.clone(),
                            lines_changed: 0,
                            has_tests: t.task_type == TaskType::Test,
                        })
                        .collect();
                    return Ok(crate::types::BatchApprovalReport {
                        scroll_id: scroll_id.to_string(),
                        task_summaries,
                        test_results: TestResults {
                            total: 0, passed: 0, failed: 0, skipped: 0,
                            output: format!("Auto-Apply failed: {}", e),
                        },
                        analysis: CodeAnalysis {
                            issues: vec![AnalysisIssue {
                                file_path: String::new(),
                                line: 0,
                                severity: Severity::Error,
                                message: format!("Auto-Apply phase failed: {}", e),
                            }],
                            metrics: AnalysisMetrics {
                                complexity: 0.0,
                                test_coverage: 0.0,
                                dependency_count: 0,
                            },
                        },
                        overall_status: AutoForgeStatus::Failed,
                        files_changed: vec![],
                        total_lines_added: 0,
                        total_lines_removed: 0,
                    });
                }
            }
        } else {
            vec![]
        };

        // Phase 1b: Trinity test generation (if available).
        //     Re-fetch tasks from DB so Trinity sees Gimli's generated code.
        //     Uses batch generation (single LLM call) when multiple tasks exist.
        if let Some(ref trinity) = self.trinity {
            let refreshed_tasks = self
                .sqlite
                .get_tasks_for_scroll(scroll_id)
                .await
                .unwrap_or_default();
            let trinity_tasks = if refreshed_tasks.is_empty() { &tasks } else { &refreshed_tasks };

            let tasks_with_code: Vec<(&crate::types::Task, &str)> = trinity_tasks
                .iter()
                .filter_map(|t| t.generated_code.as_deref().map(|code| (t, code)))
                .collect();

            if tasks_with_code.len() > 1 {
                // Batch path: single LLM call for all tasks.
                match tokio::time::timeout(
                    std::time::Duration::from_secs(180),
                    trinity.generate_tests_batch(&tasks_with_code),
                ).await {
                    Ok(Ok(results)) => {
                        for (task_id, test_code) in &results {
                            if let Some((task, _)) = tasks_with_code.iter().find(|(t, _)| t.id == *task_id) {
                                let test_path = Self::derive_test_path(&task.target_file);
                                let full_path = self.workspace_root.join(&test_path);
                                if let Some(parent) = full_path.parent() {
                                    let _ = tokio::fs::create_dir_all(parent).await;
                                }
                                if let Err(e) = tokio::fs::write(&full_path, test_code).await {
                                    warn!(task_id = %task_id, error = %e, "Failed to write batch test file in batch_execute");
                                }
                            }
                        }
                    }
                    Ok(Err(e)) => {
                        warn!(error = %e, "Trinity batch test generation failed in batch_execute");
                    }
                    Err(_) => {
                        warn!("Trinity batch test generation timed out after 180s in batch_execute");
                    }
                }
            } else {
                // Single-task path: individual generation.
                for (task, code) in &tasks_with_code {
                    match tokio::time::timeout(
                        std::time::Duration::from_secs(120),
                        trinity.generate_tests(task, code),
                    ).await {
                        Ok(Ok(test_code)) => {
                            let test_path = Self::derive_test_path(&task.target_file);
                            let full_path = self.workspace_root.join(&test_path);
                            if let Some(parent) = full_path.parent() {
                                let _ = tokio::fs::create_dir_all(parent).await;
                            }
                            if let Err(e) = tokio::fs::write(&full_path, &test_code).await {
                                warn!(task_id = %task.id, error = %e, "Failed to write test file in batch_execute");
                            }
                        }
                        Ok(Err(e)) => {
                            warn!(task_id = %task.id, error = %e, "Trinity test generation failed in batch_execute");
                        }
                        Err(_) => {
                            warn!(task_id = %task.id, "Trinity test generation timed out after 120s in batch_execute");
                        }
                    }
                }
            }
        }

        // Phase 2: Run test suite once.
        let test_results = if config.auto_test {
            self.auto_test(&config).await.unwrap_or_else(|e| TestResults {
                total: 0,
                passed: 0,
                failed: 0,
                skipped: 0,
                output: format!("Test execution failed: {}", e),
            })
        } else {
            TestResults {
                total: 0,
                passed: 0,
                failed: 0,
                skipped: 0,
                output: "Auto-Test disabled".into(),
            }
        };

        // Phase 3: Verify all changed files (Legolas-enhanced when available).
        let analysis = if config.auto_verify {
            if let Some(ref legolas) = self.legolas {
                // Legolas-enhanced verification: per-file code analysis.
                let fresh_tasks = self.sqlite.get_tasks_for_scroll(scroll_id)
                    .await
                    .unwrap_or_default();
                let mut all_issues: Vec<AnalysisIssue> = Vec::new();
                let mut total_complexity = 0.0f64;
                let mut total_dep_count = 0usize;
                let mut reviewed = 0usize;

                // Step 1: Concurrent heuristic analysis (no LLM calls).
                let mut handles = Vec::new();
                let mut files_for_llm: Vec<(String, String)> = Vec::new();
                for task in &fresh_tasks {
                    if let Some(code) = task.generated_code.clone() {
                        let legolas = Arc::clone(legolas);
                        let file_path = task.target_file.clone();
                        files_for_llm.push((file_path.clone(), code.clone()));
                        handles.push(tokio::spawn(async move {
                            let result = tokio::time::timeout(
                                std::time::Duration::from_secs(60),
                                legolas.analyze_file_heuristic(&file_path, &code),
                            ).await;
                            (file_path, result)
                        }));
                    }
                }
                for handle in handles {
                    match handle.await {
                        Ok((_, Ok(Ok(file_analysis)))) => {
                            all_issues.extend(file_analysis.issues);
                            total_complexity += file_analysis.metrics.complexity;
                            total_dep_count += file_analysis.metrics.dependency_count;
                            reviewed += 1;
                        }
                        Ok((file_path, Ok(Err(e)))) => {
                            warn!(file = %file_path, error = %e, "Legolas analysis failed in batch_execute");
                            all_issues.push(AnalysisIssue {
                                file_path,
                                line: 0,
                                severity: Severity::Warning,
                                message: format!("Code review skipped: {}", e),
                            });
                        }
                        Ok((file_path, Err(_))) => {
                            warn!(file = %file_path, "Legolas analysis timed out after 60s in batch_execute");
                            all_issues.push(AnalysisIssue {
                                file_path,
                                line: 0,
                                severity: Severity::Warning,
                                message: "Code review timed out after 60s".to_string(),
                            });
                        }
                        Err(e) => {
                            warn!(error = %e, "Legolas analysis task panicked in batch_execute");
                        }
                    }
                }

                // Step 2: Single batch LLM code review (1 sampling call).
                if !files_for_llm.is_empty() {
                    let batch_refs: Vec<(&str, &str)> = files_for_llm
                        .iter()
                        .map(|(p, c)| (p.as_str(), c.as_str()))
                        .collect();
                    match legolas.review_code_batch(&batch_refs).await {
                        Ok(batch_results) => {
                            for (file_path, llm_issues) in batch_results {
                                if !llm_issues.is_empty() {
                                    info!(
                                        file = %file_path,
                                        llm_issues = llm_issues.len(),
                                        "Legolas: LLM review found issues in batch_execute"
                                    );
                                    all_issues.extend(llm_issues);
                                    reviewed += 1;
                                }
                            }
                        }
                        Err(e) => {
                            warn!(error = %e, "Legolas batch LLM review failed in batch_execute");
                        }
                    }
                }

                CodeAnalysis {
                    issues: all_issues,
                    metrics: AnalysisMetrics {
                        complexity: if reviewed == 0 { 0.0 } else { total_complexity / reviewed as f64 },
                        test_coverage: 0.0,
                        dependency_count: total_dep_count,
                    },
                }
            } else {
                self.auto_verify(&changed_files)
                    .await
                    .unwrap_or_else(|_| CodeAnalysis {
                        issues: vec![],
                        metrics: AnalysisMetrics {
                            complexity: 0.0,
                            test_coverage: 0.0,
                            dependency_count: 0,
                        },
                    })
            }
        } else {
            CodeAnalysis {
                issues: vec![],
                metrics: AnalysisMetrics {
                    complexity: 0.0,
                    test_coverage: 0.0,
                    dependency_count: 0,
                },
            }
        };

        // Build per-task summaries.
        let task_summaries: Vec<crate::types::TaskApprovalSummary> = tasks
            .iter()
            .map(|t| {
                let lines_changed = t.diff.as_deref().map(|d| d.lines().count()).unwrap_or(0);
                crate::types::TaskApprovalSummary {
                    task_id: t.id.clone(),
                    target_file: t.target_file.clone(),
                    task_type: t.task_type.clone(),
                    status: t.status.clone(),
                    lines_changed,
                    has_tests: t.task_type == TaskType::Test,
                }
            })
            .collect();

        let overall_status = self.determine_status(&test_results, &analysis);

        let (total_added, total_removed) = Self::count_diff_lines(&tasks);

        let report = crate::types::BatchApprovalReport {
            scroll_id: scroll_id.to_string(),
            task_summaries,
            test_results,
            analysis,
            overall_status,
            files_changed: changed_files,
            total_lines_added: total_added,
            total_lines_removed: total_removed,
        };

        info!(
            scroll_id,
            tasks = report.task_summaries.len(),
            files = report.files_changed.len(),
            status = ?report.overall_status,
            "Batch approval report generated"
        );

        Ok(report)
    }

    /// Count total lines added/removed across all task diffs.
    fn count_diff_lines(tasks: &[Task]) -> (usize, usize) {
        let mut added = 0usize;
        let mut removed = 0usize;
        for task in tasks {
            if let Some(ref diff) = task.diff {
                for line in diff.lines() {
                    if line.starts_with('+') && !line.starts_with("+++") {
                        added += 1;
                    } else if line.starts_with('-') && !line.starts_with("---") {
                        removed += 1;
                    }
                }
            }
        }
        (added, removed)
    }

    // -----------------------------------------------------------------------
    // Phase 1: Auto-Apply
    // -----------------------------------------------------------------------

    /// Apply task code changes to the filesystem.
    ///
    /// Iterates over all tasks and performs the appropriate filesystem
    /// operation based on `task_type`:
    ///
    /// - **Create**: Write `generated_code` to `target_file`, creating parent
    ///   directories as needed.
    /// - **Modify**: Overwrite `target_file` with `generated_code`.
    /// - **Delete**: Remove `target_file` from the filesystem.
    /// - **Test**: Skip (test tasks are not applied as file changes).
    ///
    /// Returns the list of file paths that were changed.
    pub async fn auto_apply(&self, tasks: &[Task]) -> Result<Vec<String>> {
        let mut changed_files: Vec<String> = Vec::new();

        for task in tasks {
            let target_path = self.resolve_path(&task.target_file);

            match task.task_type {
                TaskType::Create => {
                    let code = task.generated_code.as_deref().unwrap_or("");
                    if code.is_empty() {
                        warn!(
                            task_id = task.id.as_str(),
                            file = task.target_file.as_str(),
                            "Create task has no generated code, writing empty file"
                        );
                    }

                    // Ensure parent directory exists.
                    if let Some(parent) = target_path.parent() {
                        tokio::fs::create_dir_all(parent)
                            .await
                            .with_context(|| {
                                format!(
                                    "Failed to create parent directories for {}",
                                    target_path.display()
                                )
                            })?;
                    }

                    tokio::fs::write(&target_path, code)
                        .await
                        .with_context(|| {
                            format!("Failed to write file {}", target_path.display())
                        })?;

                    debug!(
                        task_id = task.id.as_str(),
                        file = task.target_file.as_str(),
                        "Created file"
                    );
                    changed_files.push(task.target_file.clone());

                    // Aragorn already marked the task Completed; only update if
                    // it somehow isn't (defensive).
                    if task.status != TaskStatus::Completed {
                        let _ = self
                            .sqlite
                            .update_task_status(&task.id, &TaskStatus::Completed)
                            .await;
                    }
                }

                TaskType::Modify => {
                    let code = task.generated_code.as_deref().unwrap_or("");
                    if code.is_empty() {
                        warn!(
                            task_id = task.id.as_str(),
                            file = task.target_file.as_str(),
                            "Modify task has no generated code, skipping"
                        );
                        if let Err(e) = self
                            .sqlite
                            .update_task_status(&task.id, &TaskStatus::Skipped)
                            .await
                        {
                            warn!(task_id = task.id.as_str(), error = %e, "Failed to update task status");
                        }
                        continue;
                    }

                    // Ensure parent directory exists (file may have been
                    // moved to a new location).
                    if let Some(parent) = target_path.parent() {
                        tokio::fs::create_dir_all(parent).await.with_context(|| {
                            format!(
                                "Failed to create parent directories for {}",
                                target_path.display()
                            )
                        })?;
                    }

                    tokio::fs::write(&target_path, code)
                        .await
                        .with_context(|| {
                            format!("Failed to modify file {}", target_path.display())
                        })?;

                    debug!(
                        task_id = task.id.as_str(),
                        file = task.target_file.as_str(),
                        "Modified file"
                    );
                    changed_files.push(task.target_file.clone());

                    if task.status != TaskStatus::Completed {
                        let _ = self
                            .sqlite
                            .update_task_status(&task.id, &TaskStatus::Completed)
                            .await;
                    }
                }

                TaskType::Delete => {
                    if target_path.exists() {
                        tokio::fs::remove_file(&target_path)
                            .await
                            .with_context(|| {
                                format!("Failed to delete file {}", target_path.display())
                            })?;

                        debug!(
                            task_id = task.id.as_str(),
                            file = task.target_file.as_str(),
                            "Deleted file"
                        );
                        changed_files.push(task.target_file.clone());
                    } else {
                        warn!(
                            task_id = task.id.as_str(),
                            file = task.target_file.as_str(),
                            "File to delete does not exist, skipping"
                        );
                    }

                    if task.status != TaskStatus::Completed {
                        let _ = self
                            .sqlite
                            .update_task_status(&task.id, &TaskStatus::Completed)
                            .await;
                    }
                }

                TaskType::Test => {
                    debug!(
                        task_id = task.id.as_str(),
                        "Skipping test task during auto-apply"
                    );
                }
            }
        }

        info!(changed_count = changed_files.len(), "Auto-Apply finished");
        Ok(changed_files)
    }

    // -----------------------------------------------------------------------
    // Phase 2: Auto-Test
    // -----------------------------------------------------------------------

    /// Run the project's test suite and collect results.
    ///
    /// Uses the explicitly configured `test_command` from `AutoForgeConfig`,
    /// or falls back to auto-detection via `detect_test_command`. If no test
    /// command can be determined, returns a zero-count result.
    pub async fn auto_test(&self, config: &AutoForgeConfig) -> Result<TestResults> {
        let command = match config.test_command.clone() {
            Some(cmd) => Some(cmd),
            None => self.detect_test_command().await,
        };

        let command = match command {
            Some(cmd) => cmd,
            None => {
                warn!("No test command configured or detected");
                return Ok(TestResults {
                    total: 0,
                    passed: 0,
                    failed: 0,
                    skipped: 0,
                    output: "No test command available".to_string(),
                });
            }
        };

        info!(command = command.as_str(), "Running test command");

        // Split the command into program and arguments.
        let parts: Vec<&str> = command.split_whitespace().collect();
        if parts.is_empty() {
            bail!("Empty test command");
        }

        let program = parts[0];
        let args = &parts[1..];

        let output = tokio::time::timeout(
            std::time::Duration::from_secs(300),
            tokio::process::Command::new(program)
                .args(args)
                .current_dir(&self.workspace_root)
                .output()
        )
        .await
        .map_err(|_| anyhow::anyhow!("Test command timed out after 300s: {}", command))?
        .with_context(|| format!("Failed to execute test command: {}", command))?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        let combined_output = format!("{}\n{}", stdout, stderr);

        debug!(
            exit_code = output.status.code(),
            stdout_len = stdout.len(),
            stderr_len = stderr.len(),
            "Test command finished"
        );

        let results = self.parse_test_output(&combined_output, &command);

        Ok(results)
    }

    /// Detect the appropriate test command based on project files.
    ///
    /// Checks for common project configuration files in the workspace root:
    /// - `Cargo.toml` -> `cargo test`
    /// - `package.json` -> `npm test`
    /// - `pyproject.toml` or `setup.py` -> `pytest`
    /// - `Makefile` with a `test` target -> `make test`
    ///
    /// Derive a test file path from a source file path using language conventions.
    ///
    /// - Python: `test_{name}.py`
    /// - Java/Kotlin: `{Name}Test.{ext}` with `src/main/` → `src/test/`
    /// - Go: `{name}_test.go`
    /// - Ruby: `{name}_spec.rb`
    /// - Default (Rust, JS, TS, etc.): `{name}_test.{ext}`
    pub fn derive_test_path(source_path: &str) -> String {
        let path = std::path::Path::new(source_path);
        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("test");
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("rs");
        let parent = path.parent().and_then(|p| p.to_str()).unwrap_or("");

        let test_name = match ext {
            "py" => format!("test_{}.{}", stem, ext),
            "java" | "kt" => format!("{}Test.{}", stem, ext),
            "go" => format!("{}_test.{}", stem, ext),
            "rb" => format!("{}_spec.{}", stem, ext),
            _ => format!("{}_test.{}", stem, ext),
        };

        // Remap Java/Kotlin src/main → src/test
        let parent = if (ext == "java" || ext == "kt") && parent.contains("src/main/") {
            parent.replace("src/main/", "src/test/")
        } else {
            parent.to_string()
        };

        if parent.is_empty() {
            test_name
        } else {
            format!("{}/{}", parent, test_name)
        }
    }

    /// Derive a test file path, using an LLM for unknown languages.
    ///
    /// Falls back to `derive_test_path()` for known extensions. For unknown
    /// extensions, asks the LLM for the idiomatic test file convention.
    pub async fn derive_test_path_with_llm(
        source_path: &str,
        llm: &dyn crate::types::LlmProvider,
    ) -> String {
        let ext = std::path::Path::new(source_path)
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");
        let known = ["py", "java", "kt", "go", "rb", "rs", "ts", "tsx", "js", "jsx"];
        if known.contains(&ext) {
            return Self::derive_test_path(source_path);
        }
        // Ask LLM for convention
        let prompt = format!(
            "For a source file at '{}', what is the idiomatic test file path?\n\
             Reply with ONLY the test file path, nothing else.",
            source_path
        );
        match tokio::time::timeout(
            std::time::Duration::from_secs(15),
            llm.complete(&prompt),
        ).await {
            Ok(Ok(path)) => {
                let path = path.trim().to_string();
                if !path.is_empty() && path.contains('.') {
                    path
                } else {
                    Self::derive_test_path(source_path)
                }
            }
            Ok(Err(_)) | Err(_) => Self::derive_test_path(source_path),
        }
    }

    pub async fn detect_test_command(&self) -> Option<String> {
        let checks: &[(&str, &str)] = &[
            ("Cargo.toml", "cargo test"),
            ("package.json", "npm test"),
            ("pubspec.yaml", "flutter test"),
            ("go.mod", "go test ./..."),
            ("build.gradle.kts", "./gradlew test"),
            ("build.gradle", "./gradlew test"),
            ("pyproject.toml", "pytest"),
            ("setup.py", "pytest"),
            ("Makefile", "make test"),
        ];

        for (file, command) in checks {
            if self.workspace_root.join(file).exists() {
                info!(
                    detected_file = *file,
                    command = *command,
                    "Auto-detected test command"
                );
                return Some(command.to_string());
            }
        }

        // LLM fallback: ask the LLM to detect the test command from project files.
        if let Some(ref llm) = self.llm.as_ref().filter(|l| l.is_available()) {
            if let Some(cmd) = self.llm_detect_test_command(llm).await {
                return Some(cmd);
            }
        }

        None
    }

    /// Use LLM to detect the test command for unknown project types.
    ///
    /// Guarded by a 15-second timeout to prevent hangs when MCP sampling
    /// is unresponsive.
    async fn llm_detect_test_command(&self, llm: &Arc<dyn crate::types::LlmProvider>) -> Option<String> {
        // List config files in workspace root (non-recursive, top-level only).
        let config_files: Vec<String> = std::fs::read_dir(&self.workspace_root)
            .ok()?
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().ok().map(|ft| ft.is_file()).unwrap_or(false))
            .map(|e| e.file_name().to_string_lossy().to_string())
            .take(30)
            .collect();

        if config_files.is_empty() {
            return None;
        }

        let prompt = format!(
            "Given these project files in the root directory: [{}]\n\
             What is the single command to run tests? \
             Reply with ONLY the command, no explanation.",
            config_files.join(", ")
        );

        let result = tokio::time::timeout(
            std::time::Duration::from_secs(15),
            llm.complete(&prompt),
        )
        .await;

        match result {
            Ok(Ok(response)) => {
                let cmd = response.trim().to_string();
                // Validate it looks like a command (not prose).
                if !cmd.is_empty()
                    && cmd.len() < 100
                    && !cmd.contains('\n')
                    && !cmd.starts_with('[')
                {
                    info!(command = %cmd, "LLM-detected test command");
                    Some(cmd)
                } else {
                    debug!("LLM response didn't look like a command: {}", cmd);
                    None
                }
            }
            Ok(Err(e)) => {
                debug!(error = %e, "LLM test command detection failed");
                None
            }
            Err(_) => {
                warn!("LLM test command detection timed out after 15s");
                None
            }
        }
    }

    /// Parse test runner output to extract pass/fail/skip counts.
    ///
    /// Supports output from:
    /// - `cargo test` (e.g., "test result: ok. 10 passed; 2 failed; 1 ignored")
    /// - `pytest` (e.g., "5 passed, 2 failed, 1 skipped")
    /// - `npm test` / `jest` (e.g., "Tests: 2 failed, 1 skipped, 10 passed, 13 total")
    /// - Generic fallback: counts lines containing "PASS" / "FAIL" / "SKIP".
    pub fn parse_test_output(&self, output: &str, command: &str) -> TestResults {
        // Try cargo test format:
        // "test result: ok. 42 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out"
        if command.contains("cargo") {
            if let Some(results) = Self::parse_cargo_test_output(output) {
                return results;
            }
        }

        // Try pytest format:
        // "====== 5 passed, 2 failed, 1 skipped in 3.42s ======"
        if command.contains("pytest") || command.contains("python") {
            if let Some(results) = Self::parse_pytest_output(output) {
                return results;
            }
        }

        // Try jest/npm test format:
        // "Tests:       2 failed, 1 skipped, 10 passed, 13 total"
        if command.contains("npm") || command.contains("jest") || command.contains("node") {
            if let Some(results) = Self::parse_jest_output(output) {
                return results;
            }
        }

        // Try Flutter test format:
        // "00:05 +10: All tests passed!" or "00:05 +10 -2: Some tests failed."
        if command.contains("flutter") || command.contains("dart") {
            if let Some(results) = Self::parse_flutter_test_output(output) {
                return results;
            }
        }

        // Try Go test format:
        // "ok   package 0.042s" / "FAIL package 0.042s" / "--- FAIL:"
        if command.contains("go test") {
            if let Some(results) = Self::parse_go_test_output(output) {
                return results;
            }
        }

        // Generic fallback: count lines containing pass/fail keywords.
        Self::parse_generic_test_output(output)
    }

    /// Parse cargo test output.
    fn parse_cargo_test_output(output: &str) -> Option<TestResults> {
        // Pattern: "test result: ok. N passed; N failed; N ignored"
        let re = Regex::new(
            r"test result: \w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed;\s+(\d+)\s+ignored",
        )
        .ok()?;

        // There may be multiple test result lines (for different test
        // binaries), so accumulate across all of them.
        let mut total_passed: usize = 0;
        let mut total_failed: usize = 0;
        let mut total_ignored: usize = 0;

        for caps in re.captures_iter(output) {
            let passed: usize = caps.get(1)?.as_str().parse().ok()?;
            let failed: usize = caps.get(2)?.as_str().parse().ok()?;
            let ignored: usize = caps.get(3)?.as_str().parse().ok()?;

            total_passed += passed;
            total_failed += failed;
            total_ignored += ignored;
        }

        if total_passed == 0 && total_failed == 0 && total_ignored == 0 {
            return None;
        }

        Some(TestResults {
            total: total_passed + total_failed + total_ignored,
            passed: total_passed,
            failed: total_failed,
            skipped: total_ignored,
            output: output.to_string(),
        })
    }

    /// Parse pytest output.
    fn parse_pytest_output(output: &str) -> Option<TestResults> {
        let mut passed: usize = 0;
        let mut failed: usize = 0;
        let mut skipped: usize = 0;

        // Pytest summary line: "5 passed, 2 failed, 1 skipped"
        let re_passed = Regex::new(r"(\d+)\s+passed").ok()?;
        let re_failed = Regex::new(r"(\d+)\s+failed").ok()?;
        let re_skipped = Regex::new(r"(\d+)\s+skipped").ok()?;

        if let Some(caps) = re_passed.captures(output) {
            passed = caps.get(1)?.as_str().parse().ok()?;
        }
        if let Some(caps) = re_failed.captures(output) {
            failed = caps.get(1)?.as_str().parse().ok()?;
        }
        if let Some(caps) = re_skipped.captures(output) {
            skipped = caps.get(1)?.as_str().parse().ok()?;
        }

        let total = passed + failed + skipped;
        if total == 0 {
            return None;
        }

        Some(TestResults {
            total,
            passed,
            failed,
            skipped,
            output: output.to_string(),
        })
    }

    /// Parse jest / npm test output.
    fn parse_jest_output(output: &str) -> Option<TestResults> {
        // "Tests:  2 failed, 1 skipped, 10 passed, 13 total"
        let re = Regex::new(r"Tests:\s+(.+)").ok()?;

        if let Some(caps) = re.captures(output) {
            let summary = caps.get(1)?.as_str();

            let re_passed = Regex::new(r"(\d+)\s+passed").ok()?;
            let re_failed = Regex::new(r"(\d+)\s+failed").ok()?;
            let re_skipped = Regex::new(r"(\d+)\s+skipped").ok()?;
            let re_total = Regex::new(r"(\d+)\s+total").ok()?;

            let passed: usize = re_passed
                .captures(summary)
                .and_then(|c| c.get(1)?.as_str().parse().ok())
                .unwrap_or(0);
            let failed: usize = re_failed
                .captures(summary)
                .and_then(|c| c.get(1)?.as_str().parse().ok())
                .unwrap_or(0);
            let skipped: usize = re_skipped
                .captures(summary)
                .and_then(|c| c.get(1)?.as_str().parse().ok())
                .unwrap_or(0);
            let total: usize = re_total
                .captures(summary)
                .and_then(|c| c.get(1)?.as_str().parse().ok())
                .unwrap_or(passed + failed + skipped);

            if total > 0 || passed > 0 || failed > 0 {
                return Some(TestResults {
                    total,
                    passed,
                    failed,
                    skipped,
                    output: output.to_string(),
                });
            }
        }

        None
    }

    /// Parse Flutter test output.
    ///
    /// Flutter test output format: `HH:MM +N -M: message` where +N = passed, -M = failed.
    /// Final line example: "00:05 +10: All tests passed!" or "00:05 +10 -2: Some tests failed."
    fn parse_flutter_test_output(output: &str) -> Option<TestResults> {
        // Match lines like "00:05 +10: All tests passed!" or "00:05 +10 -2: Some tests failed."
        let re = Regex::new(r"(\d+:\d+)\s+\+(\d+)(?:\s+-(\d+))?:").ok()?;

        // Use the last match (final summary line).
        let mut last_passed: usize = 0;
        let mut last_failed: usize = 0;
        let mut found = false;

        for caps in re.captures_iter(output) {
            found = true;
            last_passed = caps.get(2)?.as_str().parse().ok()?;
            last_failed = caps
                .get(3)
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
        }

        if !found {
            return None;
        }

        Some(TestResults {
            total: last_passed + last_failed,
            passed: last_passed,
            failed: last_failed,
            skipped: 0,
            output: output.to_string(),
        })
    }

    /// Parse Go test output.
    ///
    /// Go test format: "ok   package 0.042s" for passing, "FAIL package" for failure,
    /// "--- PASS:" / "--- FAIL:" for individual tests.
    fn parse_go_test_output(output: &str) -> Option<TestResults> {
        let mut passed: usize = 0;
        let mut failed: usize = 0;
        let mut skipped: usize = 0;

        for line in output.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with("--- PASS:") {
                passed += 1;
            } else if trimmed.starts_with("--- FAIL:") {
                failed += 1;
            } else if trimmed.starts_with("--- SKIP:") {
                skipped += 1;
            }
        }

        let total = passed + failed + skipped;
        if total == 0 {
            return None;
        }

        Some(TestResults {
            total,
            passed,
            failed,
            skipped,
            output: output.to_string(),
        })
    }

    /// Generic fallback: count lines containing pass/fail keywords.
    fn parse_generic_test_output(output: &str) -> TestResults {
        let mut passed = 0usize;
        let mut failed = 0usize;
        let mut skipped = 0usize;

        for line in output.lines() {
            let upper = line.to_uppercase();
            if upper.contains("PASS") && !upper.contains("PASSWORD") {
                passed += 1;
            }
            if upper.contains("FAIL") {
                failed += 1;
            }
            if upper.contains("SKIP") || upper.contains("IGNORED") {
                skipped += 1;
            }
        }

        TestResults {
            total: passed + failed + skipped,
            passed,
            failed,
            skipped,
            output: output.to_string(),
        }
    }

    // -----------------------------------------------------------------------
    // Phase 3: Auto-Verify
    // -----------------------------------------------------------------------

    /// Analyze changed files for potential issues.
    ///
    /// Performs heuristic-based static analysis:
    /// - Scans for hardcoded secrets (passwords, API keys, tokens).
    /// - Counts TODO/FIXME/HACK/XXX annotations.
    /// - Detects overly long functions (> 50 lines).
    /// - Computes simple complexity and size metrics.
    ///
    /// Returns a `CodeAnalysis` containing all discovered issues and metrics.
    pub async fn auto_verify(&self, changed_files: &[String]) -> Result<CodeAnalysis> {
        let mut issues: Vec<AnalysisIssue> = Vec::new();
        let mut total_lines: usize = 0;
        let mut total_functions: usize = 0;
        let mut long_functions: usize = 0;
        let mut total_files: usize = 0;
        let mut _total_file_size: u64 = 0;
        let mut dependency_count: usize = 0;

        // Compile regex patterns once.
        let secret_regexes: Vec<Regex> = SECRET_PATTERNS
            .iter()
            .filter_map(|p| Regex::new(p).ok())
            .collect();

        let todo_regexes: Vec<Regex> = TODO_PATTERNS
            .iter()
            .filter_map(|p| Regex::new(p).ok())
            .collect();

        // Function start heuristics for various languages.
        let fn_start_re = Regex::new(
            r"(?x)
            (^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+)       |  # Rust
            (^\s*(?:async\s+)?(?:function|def)\s+\w+)      |  # JS/Python
            (^\s*(?:public|private|protected)\s+.*\s+\w+\s*\()  # Java/C#
            "
        )?;

        for file_path in changed_files {
            let abs_path = self.resolve_path(file_path);

            // Skip files that no longer exist (e.g., deleted).
            if !abs_path.exists() {
                continue;
            }

            // Read file metadata for size check.
            let metadata = tokio::fs::metadata(&abs_path).await.with_context(|| {
                format!("Failed to read metadata for {}", abs_path.display())
            })?;

            let file_size = metadata.len();
            _total_file_size += file_size;
            total_files += 1;

            if file_size > MAX_FILE_SIZE_BYTES {
                issues.push(AnalysisIssue {
                    file_path: file_path.clone(),
                    line: 0,
                    severity: Severity::Warning,
                    message: format!(
                        "File is very large ({} bytes, threshold {})",
                        file_size, MAX_FILE_SIZE_BYTES
                    ),
                });
            }

            // Read file content.
            let content = tokio::fs::read_to_string(&abs_path).await;
            let content = match content {
                Ok(c) => c,
                Err(e) => {
                    warn!(
                        file = file_path.as_str(),
                        error = %e,
                        "Failed to read file for analysis (may be binary)"
                    );
                    continue;
                }
            };

            let lines: Vec<&str> = content.lines().collect();
            total_lines += lines.len();

            // Scan each line for issues.
            for (line_num_zero, line) in lines.iter().enumerate() {
                let line_num = line_num_zero + 1;

                // Check for hardcoded secrets.
                for re in &secret_regexes {
                    if re.is_match(line) {
                        issues.push(AnalysisIssue {
                            file_path: file_path.clone(),
                            line: line_num,
                            severity: Severity::Error,
                            message: "Potential hardcoded secret detected".to_string(),
                        });
                    }
                }

                // Check for TODO/FIXME markers.
                for re in &todo_regexes {
                    if re.is_match(line) {
                        issues.push(AnalysisIssue {
                            file_path: file_path.clone(),
                            line: line_num,
                            severity: Severity::Info,
                            message: format!("TODO/FIXME marker: {}", line.trim()),
                        });
                    }
                }

                // Count dependency/import statements.
                let trimmed = line.trim();
                if trimmed.starts_with("use ")
                    || trimmed.starts_with("import ")
                    || trimmed.starts_with("from ")
                    || trimmed.starts_with("require(")
                    || trimmed.starts_with("const ") && trimmed.contains("require(")
                {
                    dependency_count += 1;
                }
            }

            // Detect function lengths.
            self.analyze_function_lengths(
                file_path,
                &lines,
                &fn_start_re,
                &mut issues,
                &mut total_functions,
                &mut long_functions,
            );
        }

        // Compute aggregate metrics.
        let complexity = Self::compute_complexity(total_lines, total_functions, long_functions);
        let test_coverage = 0.0; // Cannot measure without instrumentation.

        let metrics = AnalysisMetrics {
            complexity,
            test_coverage,
            dependency_count,
        };

        info!(
            files = total_files,
            issues = issues.len(),
            total_lines,
            total_functions,
            long_functions,
            complexity,
            "Auto-Verify analysis complete"
        );

        Ok(CodeAnalysis { issues, metrics })
    }

    /// Walk through lines looking for function definitions and measure their
    /// length. Functions exceeding `MAX_FUNCTION_LENGTH` are flagged.
    fn analyze_function_lengths(
        &self,
        file_path: &str,
        lines: &[&str],
        fn_start_re: &Regex,
        issues: &mut Vec<AnalysisIssue>,
        total_functions: &mut usize,
        long_functions: &mut usize,
    ) {
        let mut in_function = false;
        let mut fn_start_line: usize = 0;
        let mut brace_depth: i32 = 0;

        for (line_num_zero, line) in lines.iter().enumerate() {
            let line_num = line_num_zero + 1;

            if !in_function && fn_start_re.is_match(line) {
                in_function = true;
                fn_start_line = line_num;
                brace_depth = 0;
                *total_functions += 1;
            }

            if in_function {
                // Track brace depth for brace-delimited languages.
                for ch in line.chars() {
                    if ch == '{' {
                        brace_depth += 1;
                    } else if ch == '}' {
                        brace_depth -= 1;
                    }
                }

                // Function ends when braces are balanced (or we're past a
                // Python-style function with decreasing indentation -- not
                // handled here for simplicity; this heuristic works well
                // enough for Rust/JS/Java).
                if brace_depth <= 0 && line_num > fn_start_line {
                    let fn_length = line_num - fn_start_line + 1;
                    if fn_length > MAX_FUNCTION_LENGTH {
                        *long_functions += 1;
                        issues.push(AnalysisIssue {
                            file_path: file_path.to_string(),
                            line: fn_start_line,
                            severity: Severity::Warning,
                            message: format!(
                                "Function is {} lines long (threshold: {})",
                                fn_length, MAX_FUNCTION_LENGTH
                            ),
                        });
                    }
                    in_function = false;
                }
            }
        }

        // Handle an unterminated function at end of file.
        if in_function {
            let fn_length = lines.len() - fn_start_line + 1;
            if fn_length > MAX_FUNCTION_LENGTH {
                *long_functions += 1;
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: fn_start_line,
                    severity: Severity::Warning,
                    message: format!(
                        "Function is {} lines long (unterminated, threshold: {})",
                        fn_length, MAX_FUNCTION_LENGTH
                    ),
                });
            }
        }
    }

    /// Compute a simple complexity score.
    ///
    /// Higher values indicate more complex code. The score is a weighted
    /// combination of:
    /// - Lines per function (higher = more complex).
    /// - Proportion of long functions.
    ///
    /// Returns a value between 0.0 and 100.0.
    fn compute_complexity(
        total_lines: usize,
        total_functions: usize,
        long_functions: usize,
    ) -> f64 {
        if total_functions == 0 {
            return 0.0;
        }

        let avg_fn_length = total_lines as f64 / total_functions as f64;
        let long_fn_ratio = long_functions as f64 / total_functions as f64;

        // Normalize: avg_fn_length of 50 -> score 50, capped at 100.
        let length_score = (avg_fn_length / MAX_FUNCTION_LENGTH as f64) * 50.0;
        let ratio_score = long_fn_ratio * 50.0;

        (length_score + ratio_score).min(100.0)
    }

    // -----------------------------------------------------------------------
    // Phase 4: Report Generation
    // -----------------------------------------------------------------------

    /// Generate a comprehensive `AutoForgeReport` from test results and
    /// code analysis.
    pub fn generate_report(
        &self,
        scroll_id: &str,
        test_results: TestResults,
        analysis: CodeAnalysis,
    ) -> AutoForgeReport {
        self.generate_report_with_specs(scroll_id, test_results, analysis, vec![])
    }

    pub fn generate_report_with_specs(
        &self,
        scroll_id: &str,
        test_results: TestResults,
        analysis: CodeAnalysis,
        test_specs: Vec<serde_json::Value>,
    ) -> AutoForgeReport {
        self.generate_report_full(scroll_id, test_results, analysis, test_specs, vec![])
    }

    pub fn generate_report_full(
        &self,
        scroll_id: &str,
        test_results: TestResults,
        analysis: CodeAnalysis,
        test_specs: Vec<serde_json::Value>,
        code_review: Vec<CodeAnalysis>,
    ) -> AutoForgeReport {
        let status = self.determine_status(&test_results, &analysis);
        let id = ulid::Ulid::new().to_string();
        let temp_branch = format!("autoforge/{}", scroll_id);

        AutoForgeReport {
            id,
            scroll_id: scroll_id.to_string(),
            temp_branch,
            test_results,
            analysis,
            status,
            created_at: Utc::now().timestamp(),
            test_specs,
            code_review,
        }
    }

    /// Determine the overall status from test results and analysis.
    ///
    /// Decision matrix:
    /// - `AllPassed`: Tests pass (or none exist) AND no error-level issues.
    /// - `TestsFailed`: At least one test failure.
    /// - `AnalysisWarnings`: Tests pass but analysis has warning/error issues.
    /// - `Failed`: Both tests failed and analysis has errors, or other
    ///   catastrophic state.
    pub fn determine_status(
        &self,
        test_results: &TestResults,
        analysis: &CodeAnalysis,
    ) -> AutoForgeStatus {
        let tests_failed = test_results.failed > 0;

        let has_errors = analysis
            .issues
            .iter()
            .any(|i| i.severity == Severity::Error);

        let has_warnings = analysis
            .issues
            .iter()
            .any(|i| i.severity == Severity::Warning);

        if tests_failed && has_errors {
            AutoForgeStatus::Failed
        } else if tests_failed {
            AutoForgeStatus::TestsFailed
        } else if has_errors || has_warnings {
            AutoForgeStatus::AnalysisWarnings
        } else {
            AutoForgeStatus::AllPassed
        }
    }

    // -----------------------------------------------------------------------
    // Utilities
    // -----------------------------------------------------------------------

    /// Resolve a potentially relative file path against the workspace root.
    fn resolve_path(&self, file_path: &str) -> PathBuf {
        let p = Path::new(file_path);
        if p.is_absolute() {
            p.to_path_buf()
        } else {
            self.workspace_root.join(p)
        }
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use tempfile::TempDir;

    // -- Helpers ------------------------------------------------------------

    /// Create a SQLite DB in a temp directory.
    async fn setup_db() -> (Arc<SqliteDb>, TempDir) {
        let tmp = TempDir::new().expect("temp dir");
        let db_path = tmp.path().join("test_autoforge.db");
        let sqlite = Arc::new(SqliteDb::new(&db_path).expect("sqlite"));
        (sqlite, tmp)
    }

    /// Create an AutoForge instance with a temp workspace.
    async fn setup_autoforge() -> (AutoForge, Arc<SqliteDb>, TempDir) {
        let (sqlite, tmp) = setup_db().await;
        let workspace = tmp.path().to_path_buf();
        let forge = AutoForge::new(Arc::clone(&sqlite), workspace);
        (forge, sqlite, tmp)
    }

    /// Build a Task with given parameters.
    fn make_task(
        id: &str,
        scroll_id: &str,
        task_type: TaskType,
        target_file: &str,
        code: Option<&str>,
    ) -> Task {
        Task {
            id: id.to_string(),
            scroll_id: scroll_id.to_string(),
            description: format!("Task {}", id),
            target_file: target_file.to_string(),
            task_type,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: code.map(String::from),
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        }
}

    /// Seed a scroll (with optional AutoForgeConfig) and tasks into the DB.
    async fn seed_scroll(
        sqlite: &SqliteDb,
        scroll_id: &str,
        tasks: &[Task],
        config: Option<AutoForgeConfig>,
    ) {
        let now = Utc::now();
        let scroll = Scroll {
            id: scroll_id.to_string(),
            title: format!("Test Scroll {}", scroll_id),
            intent: "Testing AutoForge".to_string(),
            constraints: vec![],
            tasks: vec![],
            metadata: ScrollMetadata {
                execution_mode: ExecutionMode::AutoForge,
                auto_forge_config: config,
                recipe_used: None,
                search_context: vec![],
            },
            status: ScrollStatus::Validated,
            created_at: now,
            updated_at: now,
            cleanup_after: None,
        };
        sqlite.create_scroll(&scroll).await.expect("create scroll");

        for task in tasks {
            sqlite.create_task(task).await.expect("create task");
        }
    }

    // -- Tests: Constructor -------------------------------------------------

    #[tokio::test]
    async fn test_new_creates_instance() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;
        assert!(forge.workspace_root.exists());
    }

    // -- Tests: Auto-Apply --------------------------------------------------

    #[tokio::test]
    async fn test_auto_apply_create_file() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Create,
            "hello.txt",
            Some("Hello, AutoForge!"),
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();

        assert_eq!(changed.len(), 1);
        assert_eq!(changed[0], "hello.txt");

        // Verify the file was actually created.
        let content = tokio::fs::read_to_string(tmp.path().join("hello.txt"))
            .await
            .unwrap();
        assert_eq!(content, "Hello, AutoForge!");
    }

    #[tokio::test]
    async fn test_auto_apply_create_nested_file() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Create,
            "deep/nested/dir/file.rs",
            Some("fn main() {}"),
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();

        assert_eq!(changed.len(), 1);
        let content =
            tokio::fs::read_to_string(tmp.path().join("deep/nested/dir/file.rs"))
                .await
                .unwrap();
        assert_eq!(content, "fn main() {}");
    }

    #[tokio::test]
    async fn test_auto_apply_modify_file() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        // Pre-create the file.
        tokio::fs::write(tmp.path().join("existing.txt"), "original content")
            .await
            .unwrap();

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Modify,
            "existing.txt",
            Some("modified content"),
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();

        assert_eq!(changed.len(), 1);

        let content = tokio::fs::read_to_string(tmp.path().join("existing.txt"))
            .await
            .unwrap();
        assert_eq!(content, "modified content");
    }

    #[tokio::test]
    async fn test_auto_apply_delete_file() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        // Pre-create the file.
        tokio::fs::write(tmp.path().join("to_delete.txt"), "delete me")
            .await
            .unwrap();

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Delete,
            "to_delete.txt",
            None,
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();

        assert_eq!(changed.len(), 1);
        assert!(!tmp.path().join("to_delete.txt").exists());
    }

    #[tokio::test]
    async fn test_auto_apply_delete_nonexistent_file() {
        let (forge, sqlite, _tmp) = setup_autoforge().await;

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Delete,
            "nonexistent.txt",
            None,
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        // Should not error, but the file should not appear in changed list
        // because it did not exist.
        let changed = forge.auto_apply(&tasks).await.unwrap();
        assert!(changed.is_empty());
    }

    #[tokio::test]
    async fn test_auto_apply_skip_test_task() {
        let (forge, sqlite, _tmp) = setup_autoforge().await;

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Test,
            "test_file.rs",
            Some("test code"),
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();
        assert!(changed.is_empty());
    }

    #[tokio::test]
    async fn test_auto_apply_modify_no_code_skipped() {
        let (forge, sqlite, _tmp) = setup_autoforge().await;

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Modify,
            "somefile.txt",
            None, // no generated code
        )];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();
        assert!(changed.is_empty());
    }

    #[tokio::test]
    async fn test_auto_apply_multiple_tasks() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        // Pre-create a file for modify.
        tokio::fs::write(tmp.path().join("old.txt"), "old")
            .await
            .unwrap();

        // Pre-create a file for delete.
        tokio::fs::write(tmp.path().join("remove.txt"), "bye")
            .await
            .unwrap();

        let tasks = vec![
            make_task("t1", "s1", TaskType::Create, "new.txt", Some("new file")),
            make_task(
                "t2",
                "s1",
                TaskType::Modify,
                "old.txt",
                Some("updated"),
            ),
            make_task("t3", "s1", TaskType::Delete, "remove.txt", None),
        ];

        seed_scroll(&sqlite, "s1", &tasks, None).await;

        let changed = forge.auto_apply(&tasks).await.unwrap();

        assert_eq!(changed.len(), 3);
        assert!(tmp.path().join("new.txt").exists());
        assert!(!tmp.path().join("remove.txt").exists());

        let content = tokio::fs::read_to_string(tmp.path().join("old.txt"))
            .await
            .unwrap();
        assert_eq!(content, "updated");
    }

    // -- Tests: Test output parsing -----------------------------------------

    #[tokio::test]
    async fn test_parse_cargo_test_output() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let output = r#"
running 42 tests
test test_one ... ok
test test_two ... ok
test test_three ... FAILED

test result: ok. 39 passed; 2 failed; 1 ignored; 0 measured; 0 filtered out
"#;

        let results = forge.parse_test_output(output, "cargo test");

        assert_eq!(results.passed, 39);
        assert_eq!(results.failed, 2);
        assert_eq!(results.skipped, 1);
        assert_eq!(results.total, 42);
    }

    #[tokio::test]
    async fn test_parse_cargo_test_multiple_binaries() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let output = r#"
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

test result: ok. 5 passed; 1 failed; 2 ignored; 0 measured; 0 filtered out
"#;

        let results = forge.parse_test_output(output, "cargo test");

        assert_eq!(results.passed, 15);
        assert_eq!(results.failed, 1);
        assert_eq!(results.skipped, 2);
        assert_eq!(results.total, 18);
    }

    #[tokio::test]
    async fn test_parse_pytest_output() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let output = "====== 5 passed, 2 failed, 1 skipped in 3.42s ======";

        let results = forge.parse_test_output(output, "pytest");

        assert_eq!(results.passed, 5);
        assert_eq!(results.failed, 2);
        assert_eq!(results.skipped, 1);
        assert_eq!(results.total, 8);
    }

    #[tokio::test]
    async fn test_parse_jest_output() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let output = r#"
Test Suites: 1 failed, 2 passed, 3 total
Tests:       2 failed, 1 skipped, 10 passed, 13 total
"#;

        let results = forge.parse_test_output(output, "npm test");

        assert_eq!(results.passed, 10);
        assert_eq!(results.failed, 2);
        assert_eq!(results.skipped, 1);
        assert_eq!(results.total, 13);
    }

    #[tokio::test]
    async fn test_parse_generic_output() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let output = r#"
PASS test_a
PASS test_b
FAIL test_c
SKIP test_d
"#;

        let results = forge.parse_test_output(output, "custom-runner");

        assert_eq!(results.passed, 2);
        assert_eq!(results.failed, 1);
        assert_eq!(results.skipped, 1);
        assert_eq!(results.total, 4);
    }

    // -- Tests: Auto-Verify -------------------------------------------------

    #[tokio::test]
    async fn test_auto_verify_detects_secrets() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        let file_path = tmp.path().join("config.py");
        tokio::fs::write(
            &file_path,
            r#"
password = "super_secret_123"
api_key = "sk-1234567890abcdef"
normal_variable = 42
"#,
        )
        .await
        .unwrap();

        let analysis = forge.auto_verify(&["config.py".to_string()]).await.unwrap();

        let secret_issues: Vec<&AnalysisIssue> = analysis
            .issues
            .iter()
            .filter(|i| i.severity == Severity::Error && i.message.contains("secret"))
            .collect();

        assert!(
            secret_issues.len() >= 2,
            "Should detect at least password and api_key as secrets, found {}",
            secret_issues.len()
        );
    }

    #[tokio::test]
    async fn test_auto_verify_detects_todos() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        let file_path = tmp.path().join("code.rs");
        tokio::fs::write(
            &file_path,
            r#"
fn main() {
    // TODO: implement this properly
    // FIXME: this is broken
    println!("hello");
}
"#,
        )
        .await
        .unwrap();

        let analysis = forge.auto_verify(&["code.rs".to_string()]).await.unwrap();

        let todo_issues: Vec<&AnalysisIssue> = analysis
            .issues
            .iter()
            .filter(|i| i.message.contains("TODO/FIXME"))
            .collect();

        assert!(
            todo_issues.len() >= 2,
            "Should detect TODO and FIXME, found {}",
            todo_issues.len()
        );
    }

    #[tokio::test]
    async fn test_auto_verify_detects_long_functions() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        // Create a file with a function longer than 50 lines.
        let mut code = String::from("fn very_long_function() {\n");
        for i in 0..60 {
            code.push_str(&format!("    let x{} = {};\n", i, i));
        }
        code.push_str("}\n");

        let file_path = tmp.path().join("long.rs");
        tokio::fs::write(&file_path, &code).await.unwrap();

        let analysis = forge.auto_verify(&["long.rs".to_string()]).await.unwrap();

        let length_issues: Vec<&AnalysisIssue> = analysis
            .issues
            .iter()
            .filter(|i| i.message.contains("lines long"))
            .collect();

        assert!(
            !length_issues.is_empty(),
            "Should detect overly long function"
        );
    }

    #[tokio::test]
    async fn test_auto_verify_clean_file() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        let file_path = tmp.path().join("clean.rs");
        tokio::fs::write(
            &file_path,
            r#"
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn subtract(a: i32, b: i32) -> i32 {
    a - b
}
"#,
        )
        .await
        .unwrap();

        let analysis = forge
            .auto_verify(&["clean.rs".to_string()])
            .await
            .unwrap();

        // Clean file should have no error or warning issues.
        let serious_issues: Vec<&AnalysisIssue> = analysis
            .issues
            .iter()
            .filter(|i| i.severity == Severity::Error || i.severity == Severity::Warning)
            .collect();

        assert!(
            serious_issues.is_empty(),
            "Clean file should have no error/warning issues, found {:?}",
            serious_issues
        );
    }

    #[tokio::test]
    async fn test_auto_verify_nonexistent_file_skipped() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        // File does not exist -- should be silently skipped.
        let analysis = forge
            .auto_verify(&["ghost.txt".to_string()])
            .await
            .unwrap();

        assert!(analysis.issues.is_empty());
    }

    #[tokio::test]
    async fn test_auto_verify_counts_dependencies() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        let file_path = tmp.path().join("deps.rs");
        tokio::fs::write(
            &file_path,
            r#"
use std::sync::Arc;
use std::path::PathBuf;
use anyhow::Result;

fn do_stuff() {
    println!("stuff");
}
"#,
        )
        .await
        .unwrap();

        let analysis = forge
            .auto_verify(&["deps.rs".to_string()])
            .await
            .unwrap();

        assert!(
            analysis.metrics.dependency_count >= 3,
            "Should count at least 3 use statements, found {}",
            analysis.metrics.dependency_count
        );
    }

    // -- Tests: Status Determination ----------------------------------------

    #[tokio::test]
    async fn test_determine_status_all_passed() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10,
            passed: 10,
            failed: 0,
            skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 5.0,
                test_coverage: 0.0,
                dependency_count: 3,
            },
        };

        let status = forge.determine_status(&test_results, &analysis);
        assert_eq!(status, AutoForgeStatus::AllPassed);
    }

    #[tokio::test]
    async fn test_determine_status_tests_failed() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10,
            passed: 8,
            failed: 2,
            skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 5.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let status = forge.determine_status(&test_results, &analysis);
        assert_eq!(status, AutoForgeStatus::TestsFailed);
    }

    #[tokio::test]
    async fn test_determine_status_analysis_warnings() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10,
            passed: 10,
            failed: 0,
            skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "test.rs".to_string(),
                line: 1,
                severity: Severity::Warning,
                message: "Long function".to_string(),
            }],
            metrics: AnalysisMetrics {
                complexity: 5.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let status = forge.determine_status(&test_results, &analysis);
        assert_eq!(status, AutoForgeStatus::AnalysisWarnings);
    }

    #[tokio::test]
    async fn test_determine_status_failed() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10,
            passed: 5,
            failed: 5,
            skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "test.rs".to_string(),
                line: 1,
                severity: Severity::Error,
                message: "Hardcoded secret".to_string(),
            }],
            metrics: AnalysisMetrics {
                complexity: 80.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let status = forge.determine_status(&test_results, &analysis);
        assert_eq!(status, AutoForgeStatus::Failed);
    }

    #[tokio::test]
    async fn test_determine_status_info_only_is_all_passed() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 5,
            passed: 5,
            failed: 0,
            skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "test.rs".to_string(),
                line: 1,
                severity: Severity::Info,
                message: "TODO: improve later".to_string(),
            }],
            metrics: AnalysisMetrics {
                complexity: 5.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let status = forge.determine_status(&test_results, &analysis);
        assert_eq!(status, AutoForgeStatus::AllPassed);
    }

    // -- Tests: Report Generation -------------------------------------------

    #[tokio::test]
    async fn test_generate_report() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10,
            passed: 10,
            failed: 0,
            skipped: 0,
            output: "all good".to_string(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 5.0,
                test_coverage: 0.0,
                dependency_count: 2,
            },
        };

        let report = forge.generate_report("s1", test_results, analysis);

        assert!(!report.id.is_empty());
        assert_eq!(report.scroll_id, "s1");
        assert_eq!(report.temp_branch, "autoforge/s1");
        assert_eq!(report.status, AutoForgeStatus::AllPassed);
        assert_eq!(report.test_results.total, 10);
        assert_eq!(report.test_results.passed, 10);
        assert!(report.created_at > 0);
    }

    // -- Tests: Detect test command -----------------------------------------

    #[tokio::test]
    async fn test_detect_test_command_cargo() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        tokio::fs::write(
            tmp.path().join("Cargo.toml"),
            "[package]\nname = \"test\"",
        )
        .await
        .unwrap();

        let cmd = forge.detect_test_command().await;
        assert_eq!(cmd, Some("cargo test".to_string()));
    }

    #[tokio::test]
    async fn test_detect_test_command_npm() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        tokio::fs::write(
            tmp.path().join("package.json"),
            r#"{"name": "test"}"#,
        )
        .await
        .unwrap();

        let cmd = forge.detect_test_command().await;
        assert_eq!(cmd, Some("npm test".to_string()));
    }

    #[tokio::test]
    async fn test_detect_test_command_pytest() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        tokio::fs::write(
            tmp.path().join("pyproject.toml"),
            "[tool.pytest]\n",
        )
        .await
        .unwrap();

        let cmd = forge.detect_test_command().await;
        assert_eq!(cmd, Some("pytest".to_string()));
    }

    #[tokio::test]
    async fn test_detect_test_command_none() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let cmd = forge.detect_test_command().await;
        assert_eq!(cmd, None);
    }

    // -- Tests: Full workflow (execute) ------------------------------------

    #[tokio::test]
    async fn test_execute_full_workflow() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        let config = AutoForgeConfig {
            auto_apply: true,
            auto_test: false, // Disable to avoid needing a real test runner.
            auto_verify: true,
            max_iterations: 1,
            test_command: None,
        };

        let tasks = vec![
            make_task(
                "t1",
                "s1",
                TaskType::Create,
                "src/lib.rs",
                Some("fn add(a: i32, b: i32) -> i32 { a + b }\n"),
            ),
            make_task(
                "t2",
                "s1",
                TaskType::Create,
                "src/main.rs",
                Some("fn main() { println!(\"hello\"); }\n"),
            ),
        ];

        seed_scroll(&sqlite, "s1", &tasks, Some(config)).await;

        let report = forge.execute("s1").await.unwrap();

        assert_eq!(report.scroll_id, "s1");
        assert!(!report.id.is_empty());
        assert_eq!(report.temp_branch, "autoforge/s1");

        // Files should have been created.
        assert!(tmp.path().join("src/lib.rs").exists());
        assert!(tmp.path().join("src/main.rs").exists());
    }

    #[tokio::test]
    async fn test_execute_empty_scroll() {
        let (forge, sqlite, _tmp) = setup_autoforge().await;

        let config = AutoForgeConfig::default();
        seed_scroll(&sqlite, "s1", &[], Some(config)).await;

        let report = forge.execute("s1").await.unwrap();

        assert_eq!(report.scroll_id, "s1");
        assert_eq!(report.test_results.total, 0);
        assert!(report.analysis.issues.is_empty());
    }

    #[tokio::test]
    async fn test_execute_with_disabled_phases() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        let config = AutoForgeConfig {
            auto_apply: false,
            auto_test: false,
            auto_verify: false,
            max_iterations: 1,
            test_command: None,
        };

        let tasks = vec![make_task(
            "t1",
            "s1",
            TaskType::Create,
            "skipped.txt",
            Some("this should not be written"),
        )];

        seed_scroll(&sqlite, "s1", &tasks, Some(config)).await;

        let report = forge.execute("s1").await.unwrap();

        // File should NOT have been created because auto_apply is false.
        assert!(!tmp.path().join("skipped.txt").exists());

        // All phases disabled, so everything is "fine".
        assert_eq!(report.status, AutoForgeStatus::AllPassed);
    }

    // -- Tests: Complexity computation ------------------------------------

    #[test]
    fn test_compute_complexity_no_functions() {
        let c = AutoForge::compute_complexity(100, 0, 0);
        assert_eq!(c, 0.0);
    }

    #[test]
    fn test_compute_complexity_normal() {
        // 200 lines, 10 functions, 1 long function.
        // avg_fn_length = 20, long_fn_ratio = 0.1
        // length_score = (20/50)*50 = 20
        // ratio_score = 0.1*50 = 5
        // total = 25
        let c = AutoForge::compute_complexity(200, 10, 1);
        assert!((c - 25.0).abs() < 0.01, "Expected ~25, got {}", c);
    }

    #[test]
    fn test_compute_complexity_capped() {
        // 5000 lines, 10 functions, 10 long functions.
        // avg = 500, ratio = 1.0
        // length_score = (500/50)*50 = 500, ratio_score = 50
        // total = 550, capped at 100
        let c = AutoForge::compute_complexity(5000, 10, 10);
        assert_eq!(c, 100.0);
    }

    // -- Tests: Path resolution -------------------------------------------

    #[tokio::test]
    async fn test_resolve_path_relative() {
        let (forge, _sqlite, tmp) = setup_autoforge().await;

        let resolved = forge.resolve_path("src/main.rs");
        assert_eq!(resolved, tmp.path().join("src/main.rs"));
    }

    #[tokio::test]
    async fn test_resolve_path_absolute() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let resolved = forge.resolve_path("/absolute/path/file.rs");
        assert_eq!(resolved, PathBuf::from("/absolute/path/file.rs"));
    }

    // -- derive_test_path tests -----------------------------------------------

    #[test]
    fn test_derive_test_path_java() {
        assert_eq!(
            AutoForge::derive_test_path("src/main/java/com/vibe/ContactFormServer.java"),
            "src/test/java/com/vibe/ContactFormServerTest.java"
        );
    }

    #[test]
    fn test_derive_test_path_kotlin() {
        assert_eq!(
            AutoForge::derive_test_path("src/main/kotlin/com/app/Service.kt"),
            "src/test/kotlin/com/app/ServiceTest.kt"
        );
    }

    #[test]
    fn test_derive_test_path_python() {
        assert_eq!(
            AutoForge::derive_test_path("lib/server.py"),
            "lib/test_server.py"
        );
    }

    #[test]
    fn test_derive_test_path_rust() {
        assert_eq!(
            AutoForge::derive_test_path("src/main.rs"),
            "src/main_test.rs"
        );
    }

    #[test]
    fn test_derive_test_path_go() {
        assert_eq!(
            AutoForge::derive_test_path("pkg/server.go"),
            "pkg/server_test.go"
        );
    }

    #[test]
    fn test_derive_test_path_ruby() {
        assert_eq!(
            AutoForge::derive_test_path("app/models/user.rb"),
            "app/models/user_spec.rb"
        );
    }

    #[test]
    fn test_derive_test_path_typescript() {
        assert_eq!(
            AutoForge::derive_test_path("src/utils/helper.ts"),
            "src/utils/helper_test.ts"
        );
    }

    #[test]
    fn test_derive_test_path_no_parent() {
        assert_eq!(
            AutoForge::derive_test_path("main.rs"),
            "main_test.rs"
        );
    }

    // -- Tests: generate_report_full with code_review --

    #[tokio::test]
    async fn test_generate_report_full_includes_code_review() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 5, passed: 5, failed: 0, skipped: 0,
            output: "All passed".into(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 1.0,
                test_coverage: 0.0,
                dependency_count: 2,
            },
        };
        let code_review = vec![CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "src/foo.rs".into(),
                line: 10,
                severity: Severity::Error,
                message: "Hardcoded secret".into(),
            }],
            metrics: AnalysisMetrics {
                complexity: 0.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        }];

        let report = forge.generate_report_full("SCROLL-CR", test_results, analysis, vec![], code_review);

        assert_eq!(report.code_review.len(), 1);
        assert_eq!(report.code_review[0].issues.len(), 1);
        assert_eq!(report.code_review[0].issues[0].severity, Severity::Error);
        assert_eq!(report.code_review[0].issues[0].message, "Hardcoded secret");
    }

    #[tokio::test]
    async fn test_generate_report_full_empty_code_review() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 0, passed: 0, failed: 0, skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 0.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let report = forge.generate_report_full("SCROLL-EMPTY", test_results, analysis, vec![], vec![]);

        assert!(report.code_review.is_empty());
        assert_eq!(report.status, AutoForgeStatus::AllPassed);
    }

    // -- Tests: AutoForge status determination --

    #[tokio::test]
    async fn test_report_status_all_passed_with_code_review() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10, passed: 10, failed: 0, skipped: 0,
            output: "OK".into(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 1.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        // With empty code review, status should still be AllPassed
        let report = forge.generate_report_full("SCROLL-ST1", test_results, analysis, vec![], vec![]);
        assert_eq!(report.status, AutoForgeStatus::AllPassed);
        assert!(report.code_review.is_empty());
    }

    #[tokio::test]
    async fn test_report_status_tests_failed_with_code_review() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 10, passed: 8, failed: 2, skipped: 0,
            output: "FAIL".into(),
        };
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 1.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        let report = forge.generate_report_full("SCROLL-ST2", test_results, analysis, vec![], vec![]);
        assert_eq!(report.status, AutoForgeStatus::TestsFailed);
    }

    #[tokio::test]
    async fn test_report_status_analysis_warnings_with_review() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;

        let test_results = TestResults {
            total: 0, passed: 0, failed: 0, skipped: 0,
            output: String::new(),
        };
        let analysis = CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "src/foo.rs".into(),
                line: 1,
                severity: Severity::Warning,
                message: "TODO found".into(),
            }],
            metrics: AnalysisMetrics {
                complexity: 1.0,
                test_coverage: 0.0,
                dependency_count: 0,
            },
        };

        // Include a code review warning too
        let review = vec![CodeAnalysis {
            issues: vec![AnalysisIssue {
                file_path: "src/foo.rs".into(),
                line: 5,
                severity: Severity::Warning,
                message: "Unused variable".into(),
            }],
            metrics: AnalysisMetrics { complexity: 0.0, test_coverage: 0.0, dependency_count: 0 },
        }];

        let report = forge.generate_report_full("SCROLL-ST3", test_results, analysis, vec![], review);
        assert_eq!(report.status, AutoForgeStatus::AnalysisWarnings);
        assert_eq!(report.code_review.len(), 1);
    }

    // -- Tests: batch_execute error handling --

    #[tokio::test]
    async fn test_batch_execute_returns_error_for_missing_scroll() {
        let (forge, _sqlite, _tmp) = setup_autoforge().await;
        let result = forge.batch_execute("NONEXISTENT").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_batch_execute_with_all_phases_disabled() {
        let (forge, sqlite, _tmp) = setup_autoforge().await;

        let disabled_config = AutoForgeConfig {
            auto_apply: false,
            auto_test: false,
            auto_verify: false,
            max_iterations: 0,
            test_command: None,
        };

        let tasks = vec![
            make_task("T1", "SCROLL-B1", TaskType::Create, "src/a.rs",
                      Some("pub fn hello() {}")),
        ];
        seed_scroll(&sqlite, "SCROLL-B1", &tasks, Some(disabled_config)).await;

        let report = forge.batch_execute("SCROLL-B1").await.unwrap();

        // All phases disabled → no files changed
        assert_eq!(report.scroll_id, "SCROLL-B1");
        assert_eq!(report.task_summaries.len(), 1);
        assert!(report.files_changed.is_empty(), "No auto_apply when disabled");
    }

    #[tokio::test]
    async fn test_batch_execute_with_autoforge_enabled() {
        let (forge, sqlite, tmp) = setup_autoforge().await;

        // Create target file directory and content
        let src_dir = tmp.path().join("src");
        std::fs::create_dir_all(&src_dir).unwrap();

        let config = AutoForgeConfig {
            auto_apply: true,
            auto_test: false,
            auto_verify: false,
            max_iterations: 1,
            test_command: None,
        };
        let tasks = vec![
            make_task("T1", "SCROLL-B2", TaskType::Create, "src/new.rs",
                      Some("pub fn new_func() {}")),
        ];
        seed_scroll(&sqlite, "SCROLL-B2", &tasks, Some(config)).await;

        let report = forge.batch_execute("SCROLL-B2").await.unwrap();

        assert_eq!(report.scroll_id, "SCROLL-B2");
        assert_eq!(report.files_changed.len(), 1);
        assert!(report.files_changed[0].ends_with("src/new.rs"));
    }
}
