//! Gimli: Code Generator Agent
//!
//! Gimli generates code for SCROLL tasks. It consumes the
//! [`ConstraintContext`] prepared by Gandalf, builds a detailed LLM prompt
//! that encodes every relevant constraint, calls the LLM, post-processes
//! the response into a structured [`GeneratedCode`] artifact, and optionally
//! writes the result to disk.
//!
//! Named after the dwarven smith -- Gimli forges the actual code.

use std::path::Path;
use std::sync::Arc;

use anyhow::{Context, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};

use crate::agents::gandalf::ConstraintContext;
use crate::db::{LanceDb, SqliteDb};
use crate::error::FurnaceError;
use crate::recipes::manager::RecipeManager;
use crate::search::SearchOrchestrator;
use crate::types::{
    Constraint, ConstraintType, LlmProvider, SearchRequest, Severity, Task, TaskStatus, TaskType,
    ValidationResult,
};

// ---------------------------------------------------------------------------
// Local types
// ---------------------------------------------------------------------------

/// The output of a single code-generation pass.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedCode {
    /// Task that triggered this generation.
    pub task_id: String,

    /// Absolute or workspace-relative path to the target file.
    pub target_file: String,

    /// The full file content after generation (for Create tasks) or the
    /// new file content (for Modify tasks).
    pub code: String,

    /// A unified diff representing the change. For Create tasks this is a
    /// "new file" diff; for Modify tasks it is the delta against the
    /// existing file.
    pub diff: String,

    /// The language of the generated code (inferred from the file extension).
    pub language: String,

    /// Metadata about the generation run.
    pub metadata: GenerationMetadata,
}

/// Metadata attached to every generation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerationMetadata {
    /// How many constraints were active during generation.
    pub constraint_count: usize,

    /// How many pattern examples were provided to the LLM.
    pub example_count: usize,

    /// Approximate token count of the prompt (rough estimate).
    pub prompt_tokens_estimate: usize,

    /// ISO-8601 timestamp of generation.
    pub generated_at: String,
}

// ---------------------------------------------------------------------------
// GenerationSpec — host-delegated code generation specification
// ---------------------------------------------------------------------------

/// A fully self-contained prompt specification for host-delegated code
/// generation. Returned when MCP sampling is unavailable so the host
/// client can generate code itself.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerationSpec {
    pub task_id: String,
    pub target_file: String,
    pub task_type: TaskType,
    pub description: String,
    pub system_prompt: String,
    pub user_prompt: String,
    pub language: String,
    pub constraint_count: usize,
    pub example_count: usize,
    pub prompt_tokens_estimate: usize,
    pub similar_files: Vec<SimilarFile>,
    pub recommended_recipe: Option<String>,
}

/// A file from the LanceDb vector index that is similar to the task
/// description / target file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimilarFile {
    pub file_path: String,
    pub summary: String,
    pub similarity_score: f64,
}

// ---------------------------------------------------------------------------
// Gimli
// ---------------------------------------------------------------------------

/// The Gimli agent.
pub struct Gimli {
    llm: Arc<dyn LlmProvider>,
    sqlite: Arc<SqliteDb>,
    search: Option<Arc<SearchOrchestrator>>,
    lancedb: Option<Arc<LanceDb>>,
    recipes: Option<Arc<RecipeManager>>,
}

impl Gimli {
    /// Create a new Gimli instance.
    pub fn new(llm: Arc<dyn LlmProvider>, sqlite: Arc<SqliteDb>) -> Self {
        info!("Gimli initialized");
        Self {
            llm,
            sqlite,
            search: None,
            lancedb: None,
            recipes: None,
        }
    }

    /// Attach a SearchOrchestrator for finding additional code examples.
    pub fn with_search(mut self, search: Arc<SearchOrchestrator>) -> Self {
        self.search = Some(search);
        self
    }

    /// Attach LanceDb for vector neighbour search during spec preparation.
    pub fn with_lancedb(mut self, lancedb: Arc<LanceDb>) -> Self {
        self.lancedb = Some(lancedb);
        self
    }

    /// Attach RecipeManager for automatic recipe selection during spec preparation.
    pub fn with_recipes(mut self, recipes: Arc<RecipeManager>) -> Self {
        self.recipes = Some(recipes);
        self
    }

    /// Whether the underlying LLM provider can generate completions.
    pub fn is_llm_available(&self) -> bool {
        self.llm.is_available()
    }

    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------

    /// Generate code for a task using the given constraint context.
    ///
    /// This is the primary entry-point. It:
    /// 1. Builds a prompt from the task + context.
    /// 2. Calls the LLM.
    /// 3. Extracts the code from the LLM response.
    /// 4. Builds a diff.
    /// 5. Persists the result in SQLite.
    pub async fn generate_code(
        &self,
        task: &Task,
        context: &ConstraintContext,
    ) -> Result<GeneratedCode> {
        info!(
            task_id = %task.id,
            target = %task.target_file,
            task_type = ?task.task_type,
            "Generating code"
        );

        // 0. Enrich context with SearchOrchestrator results if available.
        let mut enriched_context = context.clone();
        if let Some(ref search) = self.search {
            let request = SearchRequest {
                query: format!("{} {}", task.description, task.target_file),
                max_results: 3,
                ..Default::default()
            };
            match tokio::time::timeout(
                std::time::Duration::from_secs(30),
                search.search(&request),
            ).await {
                Ok(Ok(response)) => {
                    let extra_count = response.results.len();
                    for result in response.results {
                        enriched_context
                            .pattern_examples
                            .push(crate::types::PatternExample {
                                file_path: result.file_path,
                                content: result.content,
                                relevance: result.relevance_score * 0.85,
                            });
                    }
                    debug!(
                        task_id = %task.id,
                        extra_examples = extra_count,
                        "Search-enriched examples added"
                    );
                }
                Ok(Err(e)) => {
                    warn!(error = %e, "SearchOrchestrator enrichment failed in Gimli");
                }
                Err(_) => {
                    warn!("SearchOrchestrator enrichment timed out after 30s in Gimli");
                }
            }
        }

        // 0b. Recipe auto-selection via LanceDb (if available and no recipe already set).
        if enriched_context.recipe_context.is_none() {
            if let (Some(ref lancedb), Some(ref recipe_mgr)) = (&self.lancedb, &self.recipes) {
                let recipe_embedding = crate::search::embedder::embed_text(&task.description);
                let search_result = tokio::time::timeout(
                    std::time::Duration::from_secs(10),
                    lancedb.search_recipes(&recipe_embedding, 1),
                ).await;
                if let Ok(Ok(results)) = search_result {
                    if let Some(top) = results.first() {
                        if top.score > 0.6 {
                            let recipe_name = &top.file_path;
                            if let Ok(Ok(recipe)) = tokio::time::timeout(
                                std::time::Duration::from_secs(5),
                                recipe_mgr.load_recipe(recipe_name),
                            ).await {
                                enriched_context.recipe_context = Some(recipe.template);
                            }
                            debug!(
                                task_id = %task.id,
                                recipe = %recipe_name,
                                score = top.score,
                                "Auto-selected recipe for code generation"
                            );
                        }
                    }
                }
            }
        }

        // 1. Build prompt.
        let prompt = self.build_prompt(task, &enriched_context);
        let prompt_tokens_est = prompt.len() / 4; // rough byte-to-token ratio

        debug!(
            task_id = %task.id,
            prompt_len = prompt.len(),
            tokens_est = prompt_tokens_est,
            "Prompt built"
        );

        // 2. Call LLM.
        if !self.llm.is_available() {
            return Err(FurnaceError::LlmProvider(
                "LLM unavailable: host does not support MCP sampling".to_string(),
            ).into());
        }
        let system = self.system_prompt(task);
        let raw_response = tokio::time::timeout(
            std::time::Duration::from_secs(120),
            self.llm.complete_with_system(&system, &prompt),
        )
        .await
        .map_err(|_| FurnaceError::LlmProvider("LLM call timed out after 120s".to_string()))?
        .map_err(|e| FurnaceError::LlmProvider(format!("LLM call failed: {}", e)))?;

        debug!(
            task_id = %task.id,
            response_len = raw_response.len(),
            "LLM response received"
        );

        // 3. Extract code from response.
        let code = Self::extract_code_block(&raw_response);

        // 4. Build diff.
        let diff = self.build_diff(task, &code);

        // 5. Detect language.
        let language = Self::detect_language(&task.target_file);

        let generated = GeneratedCode {
            task_id: task.id.clone(),
            target_file: task.target_file.clone(),
            code: code.clone(),
            diff: diff.clone(),
            language,
            metadata: GenerationMetadata {
                constraint_count: context.direct_constraints.len()
                    + context.inherited_constraints.len(),
                example_count: enriched_context.pattern_examples.len(),
                prompt_tokens_estimate: prompt_tokens_est,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        // 6. Persist to SQLite.
        if let Err(e) = self
            .sqlite
            .update_task_code(&task.id, &code, &diff)
            .await
        {
            warn!(
                task_id = %task.id,
                error = %e,
                "Failed to persist generated code to SQLite"
            );
        }

        info!(
            task_id = %task.id,
            code_lines = code.lines().count(),
            "Code generation complete"
        );

        Ok(generated)
    }

    /// Enrich a constraint context with search results and recipe auto-selection.
    ///
    /// This is the same enrichment performed in `generate_code` (steps 0 and 0b),
    /// extracted so that `generate_code_batch` can also use it.
    async fn enrich_context(&self, task: &Task, context: &ConstraintContext) -> ConstraintContext {
        let mut enriched = context.clone();

        // Search enrichment: find similar code in the workspace.
        if let Some(ref search) = self.search {
            let request = SearchRequest {
                query: format!("{} {}", task.description, task.target_file),
                max_results: 3,
                ..Default::default()
            };
            match tokio::time::timeout(
                std::time::Duration::from_secs(30),
                search.search(&request),
            )
            .await
            {
                Ok(Ok(response)) => {
                    for result in response.results {
                        enriched
                            .pattern_examples
                            .push(crate::types::PatternExample {
                                file_path: result.file_path,
                                content: result.content,
                                relevance: result.relevance_score * 0.85,
                            });
                    }
                }
                Ok(Err(e)) => {
                    warn!(error = %e, "SearchOrchestrator enrichment failed in Gimli batch");
                }
                Err(_) => {
                    warn!("SearchOrchestrator enrichment timed out in Gimli batch");
                }
            }
        }

        // Recipe auto-selection via LanceDb.
        if enriched.recipe_context.is_none() {
            if let (Some(ref lancedb), Some(ref recipe_mgr)) = (&self.lancedb, &self.recipes) {
                let recipe_embedding = crate::search::embedder::embed_text(&task.description);
                let search_result = tokio::time::timeout(
                    std::time::Duration::from_secs(10),
                    lancedb.search_recipes(&recipe_embedding, 1),
                )
                .await;
                if let Ok(Ok(results)) = search_result {
                    if let Some(top) = results.first() {
                        if top.score > 0.6 {
                            if let Ok(Ok(recipe)) = tokio::time::timeout(
                                std::time::Duration::from_secs(5),
                                recipe_mgr.load_recipe(&top.file_path),
                            )
                            .await
                            {
                                enriched.recipe_context = Some(recipe.template);
                            }
                        }
                    }
                }
            }
        }

        enriched
    }

    /// Generate code for multiple tasks in a single LLM call.
    ///
    /// Enriches each context with semantic search results (same as `generate_code`),
    /// builds a combined prompt, sends one LLM request, and parses the response to
    /// extract per-task code blocks. Falls back to individual `generate_code` calls
    /// if batch parsing fails.
    ///
    /// This reduces N sampling calls to 1, minimizing user approval prompts
    /// during autonomous execution.
    pub async fn generate_code_batch(
        &self,
        tasks: &[Task],
        contexts: &[ConstraintContext],
    ) -> Result<Vec<GeneratedCode>> {
        if tasks.is_empty() {
            return Ok(vec![]);
        }

        // Single task — delegate to normal path (includes full enrichment)
        if tasks.len() == 1 {
            let result = self.generate_code(&tasks[0], &contexts[0]).await?;
            return Ok(vec![result]);
        }

        info!(count = tasks.len(), "Gimli: batch code generation starting");

        // Enrich all contexts with search results and recipe auto-selection
        let mut enriched_contexts = Vec::with_capacity(tasks.len());
        for (task, context) in tasks.iter().zip(contexts.iter()) {
            enriched_contexts.push(self.enrich_context(task, context).await);
        }

        // Build combined prompt
        let mut combined_sections = Vec::new();
        combined_sections.push(
            "You are generating code for MULTIPLE tasks in a single response.\n\
             Output one fenced code block per task, in the SAME ORDER as listed below.\n\
             Do not skip any task. Do not combine tasks into a single block."
                .to_string(),
        );

        for (i, (task, ctx)) in tasks.iter().zip(enriched_contexts.iter()).enumerate() {
            // Inject group context: find sibling tasks in the same group.
            let group_siblings: Vec<&Task> = if task.group_label.is_some() {
                tasks.iter()
                    .filter(|t| t.id != task.id && t.group_label == task.group_label)
                    .collect()
            } else {
                vec![]
            };
            let task_prompt = if group_siblings.is_empty() {
                self.build_prompt(task, ctx)
            } else {
                self.build_prompt_with_group_context(task, ctx, &group_siblings)
            };
            combined_sections.push(format!(
                "\n---\n## Task {} of {} (ID: {})\nTarget: {}\n\n{}",
                i + 1,
                tasks.len(),
                task.id,
                task.target_file,
                task_prompt,
            ));
        }

        let combined_prompt = combined_sections.join("\n");
        let system = format!(
            "You are an expert programmer working inside the FURNACE code generation system.\n\
             You are generating code for {} tasks simultaneously.\n\
             For EACH task, output the complete file content inside its own fenced code block.\n\
             Output exactly {} fenced code blocks, one per task, in the same order as given.\n\
             Do NOT include explanations outside the code blocks.\n\
             IMPORTANT: Respond with code directly. Do not ask questions or seek clarification.\n",
            tasks.len(),
            tasks.len()
        );

        // Single LLM call for all tasks
        if !self.llm.is_available() {
            return Err(FurnaceError::LlmProvider(
                "LLM unavailable: host does not support MCP sampling".to_string(),
            ).into());
        }
        let raw_response = tokio::time::timeout(
            std::time::Duration::from_secs(180), // longer timeout for batch
            self.llm.complete_with_system(&system, &combined_prompt),
        )
        .await
        .map_err(|_| FurnaceError::LlmProvider(
            "Batch LLM call timed out after 180s".to_string(),
        ))?
        .map_err(|e| FurnaceError::LlmProvider(format!("Batch LLM call failed: {}", e)))?;

        // Parse per-task code blocks from the response
        let parsed = Self::parse_batch_response(&raw_response, tasks);

        match parsed {
            Some(code_map) if code_map.len() == tasks.len() => {
                let mut results = Vec::new();
                for (task, enriched) in tasks.iter().zip(enriched_contexts.iter()) {
                    let code = code_map.get(&task.id).cloned().unwrap_or_default();
                    let diff = self.build_diff(task, &code);
                    let language = Self::detect_language(&task.target_file);
                    let prompt_tokens_est = combined_prompt.len() / 4 / tasks.len();

                    let generated = GeneratedCode {
                        task_id: task.id.clone(),
                        target_file: task.target_file.clone(),
                        code: code.clone(),
                        diff: diff.clone(),
                        language,
                        metadata: GenerationMetadata {
                            constraint_count: enriched.direct_constraints.len()
                                + enriched.inherited_constraints.len(),
                            example_count: enriched.pattern_examples.len(),
                            prompt_tokens_estimate: prompt_tokens_est,
                            generated_at: Utc::now().to_rfc3339(),
                        },
                    };

                    // Persist to SQLite
                    if let Err(e) = self.sqlite.update_task_code(&task.id, &code, &diff).await {
                        warn!(task_id = %task.id, error = %e, "Failed to persist batch-generated code");
                    }

                    results.push(generated);
                }

                info!(count = results.len(), "Gimli: batch code generation complete");
                Ok(results)
            }
            _ => {
                // Fallback: parse failed, generate individually
                warn!("Batch response parsing failed, falling back to individual generation");
                let mut results = Vec::new();
                for (task, context) in tasks.iter().zip(contexts.iter()) {
                    match self.generate_code(task, context).await {
                        Ok(gen) => results.push(gen),
                        Err(e) => {
                            warn!(task_id = %task.id, error = %e, "Individual fallback failed");
                            return Err(e);
                        }
                    }
                }
                Ok(results)
            }
        }
    }

    /// Parse a batch LLM response into per-task code blocks.
    ///
    /// Uses two strategies:
    /// 1. **Positional matching** (primary): extract all fenced code blocks in
    ///    order and zip them with tasks by position. Reliable because we send
    ///    tasks numbered 1..N and the LLM outputs blocks in the same order.
    /// 2. **Marker-based matching** (fallback): look for
    ///    `// === TASK: <task_id> ===` markers if positional count doesn't match.
    ///
    /// Returns `None` if neither strategy produces a complete mapping.
    pub fn parse_batch_response(
        response: &str,
        tasks: &[Task],
    ) -> Option<std::collections::HashMap<String, String>> {
        // Strategy 1: Positional — extract all fenced code blocks in order
        let blocks = Self::extract_all_code_blocks(response);
        if blocks.len() == tasks.len() {
            let mut code_map = std::collections::HashMap::new();
            for (task, code) in tasks.iter().zip(blocks.into_iter()) {
                code_map.insert(task.id.clone(), code);
            }
            return Some(code_map);
        }

        // Strategy 2: Marker-based (fallback for non-standard formatting)
        let mut code_map = std::collections::HashMap::new();
        let task_ids: std::collections::HashSet<&str> =
            tasks.iter().map(|t| t.id.as_str()).collect();

        let marker_re = regex::Regex::new(r"// === TASK:\s*(\S+)\s*===").ok()?;
        let sections: Vec<&str> = marker_re.split(response).collect();
        let task_id_matches: Vec<String> = marker_re
            .captures_iter(response)
            .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
            .collect();

        for (i, task_id) in task_id_matches.iter().enumerate() {
            if !task_ids.contains(task_id.as_str()) {
                continue;
            }
            if let Some(section) = sections.get(i + 1) {
                let code = Self::extract_code_block(section);
                code_map.insert(task_id.clone(), code);
            }
        }

        if code_map.len() == tasks.len() {
            Some(code_map)
        } else {
            None
        }
    }

    /// Extract all fenced code blocks from a response, in order.
    ///
    /// Each block is the content between ``` opening and ``` closing lines.
    /// Empty blocks are skipped.
    fn extract_all_code_blocks(response: &str) -> Vec<String> {
        let mut blocks = Vec::new();
        let mut lines = response.lines().peekable();

        while let Some(line) = lines.next() {
            if line.starts_with("```") {
                let mut code_lines = Vec::new();
                for inner_line in lines.by_ref() {
                    if inner_line.starts_with("```") {
                        break;
                    }
                    code_lines.push(inner_line);
                }
                let code = code_lines.join("\n");
                // Include all blocks (even empty ones) to preserve positional
                // fidelity for batch parsing where block index maps to task index.
                blocks.push(code);
            }
        }

        blocks
    }

    /// Build the full LLM prompt for a task.
    ///
    /// The prompt is structured as:
    /// 1. Task description and target.
    /// 2. Constraint summary.
    /// 3. Related types and signatures.
    /// 4. Pattern examples.
    /// 5. Output instructions.
    pub fn build_prompt(&self, task: &Task, context: &ConstraintContext) -> String {
        let mut sections: Vec<String> = Vec::new();

        // -- Section 1: Task --
        sections.push(format!(
            "## Task\n\n\
             **ID:** {}\n\
             **Type:** {:?}\n\
             **Target file:** `{}`\n\
             **Description:** {}\n",
            task.id, task.task_type, task.target_file, task.description,
        ));

        // -- Section 2: Constraints --
        let all_constraints: Vec<&Constraint> = context
            .direct_constraints
            .iter()
            .chain(context.inherited_constraints.iter())
            .collect();

        if !all_constraints.is_empty() {
            let mut section = String::from("## Constraints\n\nThe generated code MUST satisfy the following constraints:\n\n");
            for (i, c) in all_constraints.iter().enumerate() {
                let marker = match c.constraint_type {
                    ConstraintType::Must => "MUST",
                    ConstraintType::MustNot => "MUST NOT",
                    ConstraintType::Should => "SHOULD",
                    ConstraintType::ShouldNot => "SHOULD NOT",
                };
                section.push_str(&format!(
                    "{}. **[{}]** {} (pattern: `{}`)\n",
                    i + 1,
                    marker,
                    c.description,
                    c.pattern,
                ));
            }
            sections.push(section);
        }

        // -- Section 3: Related types --
        if !context.related_types.is_empty() {
            let mut section = String::from(
                "## Related Types\n\n\
                 These types are available in scope or in dependencies. \
                 Use them where appropriate.\n\n",
            );
            for rt in &context.related_types {
                section.push_str(&format!("- `{}` ({}) from `{}`", rt.type_name, rt.kind, rt.file_path));
                if !rt.fields.is_empty() {
                    section.push_str(&format!("\n  Fields: {}", rt.fields.join(", ")));
                }
                section.push('\n');
            }
            sections.push(section);
        }

        // -- Section 4: Dependencies --
        if !context.dependency_context.is_empty() {
            let mut section = String::from(
                "## Dependencies\n\n\
                 The target file has the following import relationships:\n\n",
            );
            for dep in &context.dependency_context {
                let items = if dep.imported_items.is_empty() {
                    "*".to_string()
                } else {
                    dep.imported_items.join(", ")
                };
                section.push_str(&format!(
                    "- `{}` imports {{ {} }} from `{}`\n",
                    dep.from_file, items, dep.to_file,
                ));
            }
            sections.push(section);
        }

        // -- Section 5: Pattern examples --
        if !context.pattern_examples.is_empty() {
            let mut section = String::from(
                "## Pattern Examples\n\n\
                 Follow the style and patterns shown in these examples:\n\n",
            );
            for (i, ex) in context.pattern_examples.iter().enumerate() {
                section.push_str(&format!(
                    "### Example {} (from `{}`, relevance: {:.2})\n```\n{}\n```\n\n",
                    i + 1,
                    ex.file_path,
                    ex.relevance,
                    ex.content,
                ));
            }
            sections.push(section);
        }

        // -- Section 5.5: Recipe template --
        if let Some(ref recipe_ctx) = context.recipe_context {
            sections.push(format!(
                "## Recipe Template\n\n\
                 The following recipe template provides additional guidance for this task:\n\n\
                 {}\n",
                recipe_ctx
            ));
        }

        // -- Section 6: Output instructions --
        let output_instructions = match task.task_type {
            TaskType::Create => format!(
                "## Output Instructions\n\n\
                 Generate the COMPLETE content of the new file `{}`.\n\
                 Wrap your code in a single fenced code block with the appropriate language tag.\n\
                 Do NOT include any explanation outside the code block.\n",
                task.target_file
            ),
            TaskType::Modify => format!(
                "## Output Instructions\n\n\
                 Generate the COMPLETE updated content of `{}`.\n\
                 Incorporate the requested changes while preserving existing functionality.\n\
                 Wrap your code in a single fenced code block with the appropriate language tag.\n\
                 Do NOT include any explanation outside the code block.\n",
                task.target_file
            ),
            TaskType::Delete => format!(
                "## Output Instructions\n\n\
                 The file `{}` should be deleted. Confirm by outputting:\n\
                 ```\n// DELETE {}\n```\n",
                task.target_file, task.target_file
            ),
            TaskType::Test => format!(
                "## Output Instructions\n\n\
                 Generate a COMPLETE test file for the functionality described.\n\
                 Target test file: `{}`\n\
                 Include unit tests that cover the constraints listed above.\n\
                 Wrap your code in a single fenced code block with the appropriate language tag.\n\
                 Do NOT include any explanation outside the code block.\n",
                task.target_file
            ),
        };
        sections.push(output_instructions);

        sections.join("\n")
    }

    /// Wrap `build_prompt()` with a "Group Context" section listing sibling
    /// files in the same task group. This gives the LLM cross-file awareness
    /// for consistent imports, shared types, etc.
    pub fn build_prompt_with_group_context(
        &self,
        task: &Task,
        context: &ConstraintContext,
        group_siblings: &[&Task],
    ) -> String {
        let mut prompt = self.build_prompt(task, context);

        if !group_siblings.is_empty() {
            let mut section = format!(
                "\n## Group Context (group: {})\n\n\
                 This task is part of a group of related files. \
                 Ensure consistency with these sibling files:\n\n",
                task.group_label.as_deref().unwrap_or("unnamed")
            );
            for sibling in group_siblings {
                let has_code = if sibling.generated_code.is_some() {
                    "has code"
                } else {
                    "pending"
                };
                section.push_str(&format!(
                    "- `{}` ({:?}) — {} [{}]\n",
                    sibling.target_file, sibling.task_type, sibling.description, has_code,
                ));
            }
            section.push_str(
                "\nEnsure imports, types, and interfaces are compatible across the group.\n",
            );
            prompt.push_str(&section);
        }

        prompt
    }

    /// Apply generated code to the filesystem.
    ///
    /// For `Create` and `Modify` tasks, writes the generated code to the
    /// target file. For `Delete` tasks, removes the file.
    pub async fn apply_code(
        &self,
        generated: &GeneratedCode,
        workspace_root: &Path,
    ) -> Result<()> {
        let target = workspace_root.join(&generated.target_file);
        info!(path = %target.display(), "Applying generated code");

        // Check if this is a delete marker.
        if generated.code.trim().starts_with("// DELETE") {
            if target.exists() {
                tokio::fs::remove_file(&target)
                    .await
                    .with_context(|| format!("Failed to delete {}", target.display()))?;
                info!(path = %target.display(), "File deleted");
            } else {
                warn!(path = %target.display(), "File to delete does not exist, skipping");
            }
            return Ok(());
        }

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

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

        info!(
            path = %target.display(),
            lines = generated.code.lines().count(),
            "Code applied successfully"
        );

        // Update task status.
        if let Err(e) = self
            .sqlite
            .update_task_status(&generated.task_id, &TaskStatus::Completed)
            .await
        {
            warn!(
                task_id = %generated.task_id,
                error = %e,
                "Failed to update task status after apply"
            );
        }

        Ok(())
    }

    /// Validate generated code against the constraint set.
    ///
    /// Performs static checks on the generated source text to verify that
    /// constraints are satisfied. This is a heuristic check -- it does not
    /// compile or execute the code.
    pub async fn validate_generation(
        &self,
        generated: &GeneratedCode,
        constraints: &[Constraint],
    ) -> Result<Vec<ValidationResult>> {
        info!(
            task_id = %generated.task_id,
            constraint_count = constraints.len(),
            "Validating generated code"
        );

        let mut results = Vec::new();
        let code_lower = generated.code.to_lowercase();

        for constraint in constraints {
            let pattern_lower = constraint.pattern.to_lowercase();

            if pattern_lower.is_empty() {
                // Cannot validate constraints with no concrete pattern.
                results.push(ValidationResult {
                    constraint_id: constraint.id.clone(),
                    passed: true,
                    message: format!(
                        "Constraint '{}' has no testable pattern, skipped",
                        constraint.id
                    ),
                    severity: Severity::Info,
                });
                continue;
            }

            let pattern_found = code_lower.contains(&pattern_lower);

            match constraint.constraint_type {
                ConstraintType::Must => {
                    if pattern_found {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true,
                            message: format!(
                                "MUST constraint satisfied: pattern '{}' found",
                                constraint.pattern
                            ),
                            severity: Severity::Info,
                        });
                    } else {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: false,
                            message: format!(
                                "MUST constraint violated: pattern '{}' not found in generated code. {}",
                                constraint.pattern, constraint.description
                            ),
                            severity: Severity::Error,
                        });
                    }
                }
                ConstraintType::MustNot => {
                    if pattern_found {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: false,
                            message: format!(
                                "MUST NOT constraint violated: pattern '{}' found in generated code. {}",
                                constraint.pattern, constraint.description
                            ),
                            severity: Severity::Error,
                        });
                    } else {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true,
                            message: format!(
                                "MUST NOT constraint satisfied: pattern '{}' absent",
                                constraint.pattern
                            ),
                            severity: Severity::Info,
                        });
                    }
                }
                ConstraintType::Should => {
                    if pattern_found {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true,
                            message: format!(
                                "SHOULD constraint satisfied: pattern '{}' found",
                                constraint.pattern
                            ),
                            severity: Severity::Info,
                        });
                    } else {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true, // advisory -- does not fail
                            message: format!(
                                "SHOULD constraint advisory: pattern '{}' not found. {}",
                                constraint.pattern, constraint.description
                            ),
                            severity: Severity::Warning,
                        });
                    }
                }
                ConstraintType::ShouldNot => {
                    if pattern_found {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true, // advisory -- does not fail
                            message: format!(
                                "SHOULD NOT advisory: pattern '{}' found. {}",
                                constraint.pattern, constraint.description
                            ),
                            severity: Severity::Warning,
                        });
                    } else {
                        results.push(ValidationResult {
                            constraint_id: constraint.id.clone(),
                            passed: true,
                            message: format!(
                                "SHOULD NOT constraint satisfied: pattern '{}' absent",
                                constraint.pattern
                            ),
                            severity: Severity::Info,
                        });
                    }
                }
            }
        }

        let violations = results.iter().filter(|r| !r.passed).count();
        let warnings = results
            .iter()
            .filter(|r| r.severity == Severity::Warning)
            .count();

        info!(
            task_id = %generated.task_id,
            total = results.len(),
            violations = violations,
            warnings = warnings,
            "Validation complete"
        );

        Ok(results)
    }

    /// Prepare a self-contained generation specification for host-delegated
    /// code generation.
    ///
    /// Used when MCP sampling is unavailable: builds a rich prompt from
    /// Gandalf context, LanceDb neighbours, and auto-selected recipes,
    /// then returns a `GenerationSpec` the host can use to generate code.
    pub async fn prepare_generation_spec(
        &self,
        task: &Task,
        context: &ConstraintContext,
    ) -> Result<GenerationSpec> {
        info!(
            task_id = %task.id,
            target = %task.target_file,
            "Preparing generation spec (host-delegated)"
        );

        // 1. Skip SearchOrchestrator enrichment here — prepare_generation_spec
        //    is only called when LLM is unavailable, and SearchOrchestrator's
        //    query classifier / result judge may trigger LLM calls that would
        //    fail. LanceDb neighbour search (step 2) provides similar files
        //    and Gandalf's constraint context already has pattern examples.
        let mut enriched_context = context.clone();

        // 2. LanceDb neighbour search — find similar files.
        let mut similar_files = Vec::new();
        if let Some(ref lancedb) = self.lancedb {
            let query_text = format!("{} {}", task.description, task.target_file);
            let embedding = crate::search::embedder::embed_text(&query_text);
            match tokio::time::timeout(
                std::time::Duration::from_secs(10),
                lancedb.search(&embedding, 5),
            ).await {
                Ok(Ok(results)) => {
                    for r in results {
                        similar_files.push(SimilarFile {
                            file_path: r.file_path.clone(),
                            summary: r.content.chars().take(500).collect(),
                            similarity_score: r.score,
                        });
                    }
                }
                Ok(Err(e)) => {
                    warn!(error = %e, "LanceDb neighbour search failed in Gimli");
                }
                Err(_) => {
                    warn!("LanceDb neighbour search timed out after 10s in Gimli");
                }
            }
        }

        // 3. LanceDb recipe auto-selection — pick best matching recipe.
        let mut recommended_recipe: Option<String> = None;
        if let (Some(ref lancedb), Some(ref recipe_mgr)) = (&self.lancedb, &self.recipes) {
            let recipe_embedding = crate::search::embedder::embed_text(&task.description);
            let search_result = tokio::time::timeout(
                std::time::Duration::from_secs(10),
                lancedb.search_recipes(&recipe_embedding, 1),
            ).await;
            if let Ok(Ok(results)) = search_result {
                if let Some(top) = results.first() {
                    if top.score > 0.6 {
                        // Auto-select this recipe and include its raw template.
                        // We use load_recipe() instead of render_recipe() because
                        // render_recipe() requires variable values for required
                        // variables — which we don't have at spec-preparation time.
                        // The raw template with {{placeholders}} is useful context
                        // for the host LLM to follow the recipe's structure.
                        let recipe_name = &top.file_path; // stored as "name" in recipe table
                        recommended_recipe = Some(recipe_name.clone());
                        if let Ok(Ok(recipe)) = tokio::time::timeout(
                            std::time::Duration::from_secs(5),
                            recipe_mgr.load_recipe(recipe_name),
                        ).await {
                            enriched_context.recipe_context = Some(recipe.template);
                        }
                        debug!(
                            recipe = %recipe_name,
                            score = top.score,
                            "Auto-selected recipe for generation spec"
                        );
                    } else {
                        debug!(
                            recipe = %top.file_path,
                            score = top.score,
                            threshold = 0.6,
                            "Recipe score below threshold, skipping auto-selection"
                        );
                    }
                }
            }
        }

        // 4. Build prompts.
        let user_prompt = self.build_prompt(task, &enriched_context);
        let system_prompt = self.system_prompt(task);
        let prompt_tokens_estimate = user_prompt.len() / 4;
        let language = Self::detect_language(&task.target_file);

        let constraint_count = context.direct_constraints.len()
            + context.inherited_constraints.len();
        let example_count = enriched_context.pattern_examples.len();

        let spec = GenerationSpec {
            task_id: task.id.clone(),
            target_file: task.target_file.clone(),
            task_type: task.task_type.clone(),
            description: task.description.clone(),
            system_prompt,
            user_prompt,
            language,
            constraint_count,
            example_count,
            prompt_tokens_estimate,
            similar_files,
            recommended_recipe,
        };

        info!(
            task_id = %task.id,
            prompt_tokens = prompt_tokens_estimate,
            similar_files = spec.similar_files.len(),
            recipe = ?spec.recommended_recipe,
            "Generation spec prepared"
        );

        Ok(spec)
    }

    // -----------------------------------------------------------------------
    // Internal helpers
    // -----------------------------------------------------------------------

    /// Build a system prompt that steers the LLM toward high-quality
    /// code generation.
    fn system_prompt(&self, task: &Task) -> String {
        let lang = Self::detect_language(&task.target_file);
        format!(
            "You are an expert {} programmer working inside the FURNACE code generation system.\n\
             Your job is to generate production-quality code that satisfies ALL stated constraints.\n\
             \n\
             Rules:\n\
             1. Output ONLY the code inside a single fenced code block.\n\
             2. Do NOT include explanations, commentary, or markdown outside the code block.\n\
             3. Follow every MUST constraint exactly. Violating a MUST constraint is a critical error.\n\
             4. Avoid every MUST NOT pattern. Including a MUST NOT pattern is a critical error.\n\
             5. Follow SHOULD constraints when possible.\n\
             6. Match the style and patterns of the provided examples.\n\
             7. Include appropriate error handling -- never panic in production code.\n\
             8. Preserve existing public API signatures when modifying files.\n\
             \n\
             IMPORTANT: Respond with code directly. Do not ask questions, seek clarification, \
             or request approval. Generate the best implementation based on the information provided.\n",
            lang
        )
    }

    /// Extract the first fenced code block from an LLM response.
    ///
    /// If no code block is found, returns the entire response (the LLM
    /// may have omitted fences).
    pub(crate) fn extract_code_block(response: &str) -> String {
        // Look for ```<lang>\n...\n```
        let lines: Vec<&str> = response.lines().collect();
        let mut in_block = false;
        let mut code_lines: Vec<&str> = Vec::new();
        let mut found_block = false;

        for line in &lines {
            if !in_block && line.starts_with("```") {
                in_block = true;
                found_block = true;
                continue;
            }
            if in_block && line.starts_with("```") {
                break;
            }
            if in_block {
                code_lines.push(line);
            }
        }

        if found_block {
            // Fenced block was found — return its content (may be empty).
            code_lines.join("\n")
        } else {
            // No fenced block found; return the raw response trimmed.
            // The LLM may have omitted fences entirely.
            response.trim().to_string()
        }
    }

    /// Build a unified-diff-style string for the change.
    fn build_diff(&self, task: &Task, new_code: &str) -> String {
        match task.task_type {
            TaskType::Create => {
                let mut diff = format!(
                    "--- /dev/null\n+++ b/{}\n@@ -0,0 +1,{} @@\n",
                    task.target_file,
                    new_code.lines().count()
                );
                for line in new_code.lines() {
                    diff.push('+');
                    diff.push_str(line);
                    diff.push('\n');
                }
                diff
            }
            TaskType::Modify => {
                // Without the old file content we can only show the full new
                // content as an "replace entire file" diff.
                let line_count = new_code.lines().count();
                let mut diff = format!(
                    "--- a/{}\n+++ b/{}\n@@ -1,0 +1,{} @@\n",
                    task.target_file, task.target_file, line_count
                );
                for line in new_code.lines() {
                    diff.push('+');
                    diff.push_str(line);
                    diff.push('\n');
                }
                diff
            }
            TaskType::Delete => {
                format!(
                    "--- a/{}\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-// File deleted\n",
                    task.target_file
                )
            }
            TaskType::Test => {
                let line_count = new_code.lines().count();
                let mut diff = format!(
                    "--- /dev/null\n+++ b/{}\n@@ -0,0 +1,{} @@\n",
                    task.target_file, line_count
                );
                for line in new_code.lines() {
                    diff.push('+');
                    diff.push_str(line);
                    diff.push('\n');
                }
                diff
            }
        }
    }

    /// Convert a `GenerationSpec` into a `PrefetchPrompt`.
    pub fn generation_spec_to_prompt(spec: &GenerationSpec) -> crate::mcp::prefetch::PrefetchPrompt {
        crate::mcp::prefetch::PrefetchPrompt {
            id: format!("gimli-gen-{}", spec.task_id),
            agent: "Gimli".into(),
            task_id: Some(spec.task_id.clone()),
            system_prompt: spec.system_prompt.clone(),
            user_prompt: spec.user_prompt.clone(),
            description: format!(
                "Generate {} code for {}",
                spec.language, spec.target_file
            ),
        }
    }

    /// Detect the programming language from a file extension.
    fn detect_language(file_path: &str) -> String {
        Path::new(file_path)
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| match ext {
                "rs" => "Rust",
                "ts" => "TypeScript",
                "tsx" => "TypeScript (React)",
                "js" => "JavaScript",
                "jsx" => "JavaScript (React)",
                "py" => "Python",
                "go" => "Go",
                "java" => "Java",
                "kt" => "Kotlin",
                "swift" => "Swift",
                "c" => "C",
                "cpp" | "cc" | "cxx" => "C++",
                "h" | "hpp" => "C/C++ Header",
                "rb" => "Ruby",
                "cs" => "C#",
                "sql" => "SQL",
                "toml" => "TOML",
                "yaml" | "yml" => "YAML",
                "json" => "JSON",
                "md" => "Markdown",
                "sh" | "bash" => "Bash",
                _ => ext,
            })
            .unwrap_or("Unknown")
            .to_string()
    }

}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::gandalf::{ConstraintContext, DependencyInfo, RelatedType};
    use crate::types::*;
    use async_trait::async_trait;
    use tempfile::TempDir;

    // -- Mock LLM provider --

    struct MockLlm {
        response: String,
    }

    impl MockLlm {
        fn new(response: &str) -> Self {
            Self {
                response: response.to_string(),
            }
        }
    }

    #[async_trait]
    impl LlmProvider for MockLlm {
        async fn complete(&self, _prompt: &str) -> anyhow::Result<String> {
            Ok(self.response.clone())
        }

        async fn complete_with_system(&self, _system: &str, _prompt: &str) -> anyhow::Result<String> {
            Ok(self.response.clone())
        }
    }

    // -- Helpers --

    async fn setup(llm_response: &str) -> (Arc<SqliteDb>, Gimli, TempDir) {
        let tmp = TempDir::new().expect("temp dir");
        let db_path = tmp.path().join("test.db");
        let sqlite = Arc::new(SqliteDb::new(&db_path).expect("sqlite"));
        let llm: Arc<dyn LlmProvider> = Arc::new(MockLlm::new(llm_response));
        let gimli = Gimli::new(llm, Arc::clone(&sqlite));
        (sqlite, gimli, tmp)
    }

    fn sample_task() -> Task {
        Task {
            id: "task-g-001".into(),
            scroll_id: "SCROLL-001".into(),
            description: "Create auth handler with JWT validation".into(),
            target_file: "src/handlers/auth.rs".into(),
            task_type: TaskType::Create,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: None,
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        }
    }

    fn sample_context() -> ConstraintContext {
        ConstraintContext {
            direct_constraints: vec![
                Constraint {
                    id: "c-must-async".into(),
                    constraint_type: ConstraintType::Must,
                    description: "Use async/await".into(),
                    pattern: "async".into(),
                    source: ConstraintSource::UserSpecified,
                },
                Constraint {
                    id: "c-no-unwrap".into(),
                    constraint_type: ConstraintType::MustNot,
                    description: "No unwrap".into(),
                    pattern: "unwrap()".into(),
                    source: ConstraintSource::UserSpecified,
                },
            ],
            inherited_constraints: vec![],
            pattern_examples: vec![PatternExample {
                file_path: "src/handlers/health.rs".into(),
                content: "pub async fn health_check() -> Result<Response> {\n    Ok(Response::ok())\n}".into(),
                relevance: 0.9,
            }],
            related_types: vec![RelatedType {
                file_path: "src/types.rs".into(),
                type_name: "Response".into(),
                kind: "struct".into(),
                fields: vec!["status: u16".into(), "body: String".into()],
            }],
            dependency_context: vec![DependencyInfo {
                from_file: "src/handlers/auth.rs".into(),
                to_file: "src/types.rs".into(),
                imported_items: vec!["Request".into(), "Response".into()],
            }],
            summary: "Task context summary".into(),
            recipe_context: None,
        }
    }

    async fn seed_scroll_for_gimli(sqlite: &SqliteDb) {
        let now = Utc::now();
        let scroll = Scroll {
            id: "SCROLL-001".into(),
            title: "Auth".into(),
            intent: "Add auth".into(),
            constraints: vec![],
            tasks: vec![],
            metadata: ScrollMetadata {
                execution_mode: ExecutionMode::Forge,
                auto_forge_config: None,
                recipe_used: None,
                search_context: vec![],
            },
            status: ScrollStatus::Draft,
            created_at: now,
            updated_at: now,
            cleanup_after: None,
        };
        sqlite.create_scroll(&scroll).await.expect("scroll");

        let task = sample_task();
        sqlite.create_task(&task).await.expect("task");
    }

    // -- Tests --

    #[tokio::test]
    async fn test_build_prompt_includes_constraints() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(prompt.contains("## Task"), "Prompt should have Task section");
        assert!(
            prompt.contains("## Constraints"),
            "Prompt should have Constraints section"
        );
        assert!(
            prompt.contains("MUST") && prompt.contains("async"),
            "Prompt should contain MUST async constraint"
        );
        assert!(
            prompt.contains("MUST NOT") && prompt.contains("unwrap()"),
            "Prompt should contain MUST NOT unwrap constraint"
        );
    }

    #[tokio::test]
    async fn test_build_prompt_includes_examples() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(
            prompt.contains("## Pattern Examples"),
            "Prompt should have examples section"
        );
        assert!(
            prompt.contains("health_check"),
            "Prompt should reference the example code"
        );
    }

    #[tokio::test]
    async fn test_build_prompt_includes_types() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(
            prompt.contains("## Related Types"),
            "Prompt should have related types section"
        );
        assert!(
            prompt.contains("Response"),
            "Prompt should reference the Response type"
        );
    }

    #[tokio::test]
    async fn test_build_prompt_includes_dependencies() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(
            prompt.contains("## Dependencies"),
            "Prompt should have dependencies section"
        );
        assert!(
            prompt.contains("src/types.rs"),
            "Prompt should reference dependency on types.rs"
        );
    }

    #[tokio::test]
    async fn test_build_prompt_output_instructions_create() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(
            prompt.contains("COMPLETE content of the new file"),
            "Create task should instruct full file generation"
        );
    }

    #[tokio::test]
    async fn test_build_prompt_output_instructions_modify() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let mut task = sample_task();
        task.task_type = TaskType::Modify;
        let ctx = sample_context();

        let prompt = gimli.build_prompt(&task, &ctx);

        assert!(
            prompt.contains("COMPLETE updated content"),
            "Modify task should instruct updated file generation"
        );
    }

    #[tokio::test]
    async fn test_extract_code_block_fenced() {
        let response = "Here is the code:\n\n```rust\nfn main() {\n    println!(\"hello\");\n}\n```\n\nDone.";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "fn main() {\n    println!(\"hello\");\n}");
    }

    #[tokio::test]
    async fn test_extract_code_block_no_fence() {
        let response = "fn main() {\n    println!(\"hello\");\n}";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "fn main() {\n    println!(\"hello\");\n}");
    }

    #[tokio::test]
    async fn test_extract_code_block_multiple_blocks_takes_first() {
        let response = "```rust\nfirst()\n```\n\n```rust\nsecond()\n```";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "first()");
    }

    #[tokio::test]
    async fn test_generate_code_end_to_end() {
        let llm_response = "```rust\npub async fn authenticate(req: Request) -> Result<Response> {\n    let token = req.headers.get(\"Authorization\")\n        .ok_or(AuthError::Missing)?;\n    validate_jwt(token).await\n}\n```";

        let (sqlite, gimli, _tmp) = setup(llm_response).await;
        seed_scroll_for_gimli(&sqlite).await;

        let task = sample_task();
        let ctx = sample_context();

        let generated = gimli.generate_code(&task, &ctx).await.unwrap();

        assert_eq!(generated.task_id, "task-g-001");
        assert_eq!(generated.target_file, "src/handlers/auth.rs");
        assert!(generated.code.contains("async fn authenticate"));
        assert_eq!(generated.language, "Rust");
        assert!(!generated.diff.is_empty());
        assert!(generated.metadata.constraint_count > 0);
    }

    #[tokio::test]
    async fn test_apply_code_create() {
        let (sqlite, gimli, tmp) = setup("").await;

        let workspace = tmp.path().join("workspace");
        tokio::fs::create_dir_all(&workspace).await.unwrap();

        seed_scroll_for_gimli(&sqlite).await;

        let generated = GeneratedCode {
            task_id: "task-g-001".into(),
            target_file: "src/handlers/auth.rs".into(),
            code: "pub fn hello() {}\n".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 0,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        gimli.apply_code(&generated, &workspace).await.unwrap();

        let written = tokio::fs::read_to_string(workspace.join("src/handlers/auth.rs"))
            .await
            .unwrap();
        assert_eq!(written, "pub fn hello() {}\n");
    }

    #[tokio::test]
    async fn test_apply_code_delete() {
        let (sqlite, gimli, tmp) = setup("").await;

        let workspace = tmp.path().join("workspace");
        let target = workspace.join("src/handlers/obsolete.rs");
        tokio::fs::create_dir_all(target.parent().unwrap()).await.unwrap();
        tokio::fs::write(&target, "old code").await.unwrap();

        seed_scroll_for_gimli(&sqlite).await;

        let generated = GeneratedCode {
            task_id: "task-g-001".into(),
            target_file: "src/handlers/obsolete.rs".into(),
            code: "// DELETE src/handlers/obsolete.rs".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 0,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        gimli.apply_code(&generated, &workspace).await.unwrap();
        assert!(!target.exists(), "File should have been deleted");
    }

    #[tokio::test]
    async fn test_validate_generation_must_satisfied() {
        let (_, gimli, _tmp) = setup("").await;

        let generated = GeneratedCode {
            task_id: "t1".into(),
            target_file: "test.rs".into(),
            code: "pub async fn do_work() -> Result<()> { Ok(()) }".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 1,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        let constraints = vec![Constraint {
            id: "c-async".into(),
            constraint_type: ConstraintType::Must,
            description: "Use async".into(),
            pattern: "async".into(),
            source: ConstraintSource::UserSpecified,
        }];

        let results = gimli
            .validate_generation(&generated, &constraints)
            .await
            .unwrap();

        assert!(
            results.iter().all(|r| r.passed),
            "Expected MUST async to pass: {:?}",
            results
        );
    }

    #[tokio::test]
    async fn test_validate_generation_must_violated() {
        let (_, gimli, _tmp) = setup("").await;

        let generated = GeneratedCode {
            task_id: "t1".into(),
            target_file: "test.rs".into(),
            code: "pub fn do_work() -> Result<()> { Ok(()) }".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 1,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        let constraints = vec![Constraint {
            id: "c-async".into(),
            constraint_type: ConstraintType::Must,
            description: "Use async".into(),
            pattern: "async".into(),
            source: ConstraintSource::UserSpecified,
        }];

        let results = gimli
            .validate_generation(&generated, &constraints)
            .await
            .unwrap();

        assert!(
            results.iter().any(|r| !r.passed),
            "Expected MUST async to fail: {:?}",
            results
        );
    }

    #[tokio::test]
    async fn test_validate_generation_must_not_violated() {
        let (_, gimli, _tmp) = setup("").await;

        let generated = GeneratedCode {
            task_id: "t1".into(),
            target_file: "test.rs".into(),
            code: "let val = opt.unwrap();".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 1,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        let constraints = vec![Constraint {
            id: "c-no-unwrap".into(),
            constraint_type: ConstraintType::MustNot,
            description: "No unwrap".into(),
            pattern: "unwrap()".into(),
            source: ConstraintSource::UserSpecified,
        }];

        let results = gimli
            .validate_generation(&generated, &constraints)
            .await
            .unwrap();

        assert!(
            results.iter().any(|r| !r.passed && r.severity == Severity::Error),
            "Expected MUST NOT unwrap to fail: {:?}",
            results
        );
    }

    #[tokio::test]
    async fn test_validate_generation_should_advisory() {
        let (_, gimli, _tmp) = setup("").await;

        let generated = GeneratedCode {
            task_id: "t1".into(),
            target_file: "test.rs".into(),
            code: "fn sync_work() {}".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 1,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        let constraints = vec![Constraint {
            id: "c-should-docs".into(),
            constraint_type: ConstraintType::Should,
            description: "Should have doc comments".into(),
            pattern: "///".into(),
            source: ConstraintSource::Inferred,
        }];

        let results = gimli
            .validate_generation(&generated, &constraints)
            .await
            .unwrap();

        // SHOULD constraints are advisory; the validation should pass
        // but with a warning.
        assert!(
            results.iter().all(|r| r.passed),
            "SHOULD constraints should not fail validation: {:?}",
            results
        );
        assert!(
            results.iter().any(|r| r.severity == Severity::Warning),
            "Expected a warning for unmet SHOULD constraint: {:?}",
            results
        );
    }

    #[tokio::test]
    async fn test_detect_language() {
        assert_eq!(Gimli::detect_language("foo.rs"), "Rust");
        assert_eq!(Gimli::detect_language("bar.ts"), "TypeScript");
        assert_eq!(Gimli::detect_language("baz.py"), "Python");
        assert_eq!(Gimli::detect_language("qux.go"), "Go");
        assert_eq!(Gimli::detect_language("no_extension"), "Unknown");
    }

    #[tokio::test]
    async fn test_diff_create_task() {
        let (_, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let code = "line1\nline2\nline3";

        let diff = gimli.build_diff(&task, code);

        assert!(diff.contains("--- /dev/null"));
        assert!(diff.contains("+++ b/src/handlers/auth.rs"));
        assert!(diff.contains("+line1"));
        assert!(diff.contains("+line2"));
        assert!(diff.contains("+line3"));
    }

    #[tokio::test]
    async fn test_diff_delete_task() {
        let (_, gimli, _tmp) = setup("").await;
        let mut task = sample_task();
        task.task_type = TaskType::Delete;

        let diff = gimli.build_diff(&task, "");

        assert!(diff.contains("+++ /dev/null"));
        assert!(diff.contains("-// File deleted"));
    }

    #[tokio::test]
    async fn test_system_prompt_references_language() {
        let (_, gimli, _tmp) = setup("").await;
        let task = sample_task();

        let system = gimli.system_prompt(&task);
        assert!(
            system.contains("Rust"),
            "System prompt should mention the language"
        );
        assert!(
            system.contains("MUST constraint"),
            "System prompt should reference constraint rules"
        );
    }

    // -- Tests: Host-delegated code generation --

    #[tokio::test]
    async fn test_prepare_generation_spec_builds_valid_spec() {
        let (sqlite, gimli, _tmp) = setup("").await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let ctx = sample_context();

        let spec = gimli.prepare_generation_spec(&task, &ctx).await.unwrap();

        assert_eq!(spec.task_id, "task-g-001");
        assert_eq!(spec.target_file, "src/handlers/auth.rs");
        assert_eq!(spec.task_type, TaskType::Create);
        assert!(!spec.system_prompt.is_empty(), "System prompt should be non-empty");
        assert!(!spec.user_prompt.is_empty(), "User prompt should be non-empty");
        assert_eq!(spec.language, "Rust");
        assert_eq!(spec.constraint_count, 2); // 2 direct + 0 inherited
        assert!(spec.example_count >= 1); // at least the sample_context example
        assert!(spec.prompt_tokens_estimate > 0);
    }

    #[tokio::test]
    async fn test_prepare_generation_spec_includes_constraints_in_prompt() {
        let (sqlite, gimli, _tmp) = setup("").await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let ctx = sample_context();

        let spec = gimli.prepare_generation_spec(&task, &ctx).await.unwrap();

        assert!(spec.user_prompt.contains("MUST"), "Spec prompt should include MUST constraints");
        assert!(spec.user_prompt.contains("async"), "Spec prompt should include constraint patterns");
        assert!(spec.user_prompt.contains("## Output Instructions"), "Spec prompt should include output instructions");
    }

    #[tokio::test]
    async fn test_prepare_generation_spec_with_empty_context() {
        let (sqlite, gimli, _tmp) = setup("").await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let empty_ctx = ConstraintContext {
            direct_constraints: vec![],
            inherited_constraints: vec![],
            pattern_examples: vec![],
            related_types: vec![],
            dependency_context: vec![],
            summary: String::new(),
            recipe_context: None,
        };

        let spec = gimli.prepare_generation_spec(&task, &empty_ctx).await.unwrap();

        assert_eq!(spec.constraint_count, 0);
        assert_eq!(spec.example_count, 0);
        assert!(!spec.user_prompt.contains("## Constraints"));
        assert!(spec.user_prompt.contains("## Task"));
        assert!(spec.user_prompt.contains("## Output Instructions"));
    }

    #[tokio::test]
    async fn test_prepare_generation_spec_with_recipe_context() {
        let (sqlite, gimli, _tmp) = setup("").await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let mut ctx = sample_context();
        ctx.recipe_context = Some("Use the builder pattern for all constructors".into());

        let spec = gimli.prepare_generation_spec(&task, &ctx).await.unwrap();

        assert!(spec.user_prompt.contains("## Recipe Template"));
        assert!(spec.user_prompt.contains("builder pattern"));
    }

    #[tokio::test]
    async fn test_prepare_generation_spec_no_similar_files_without_lancedb() {
        let (sqlite, gimli, _tmp) = setup("").await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let ctx = sample_context();

        // Gimli constructed without LanceDb
        let spec = gimli.prepare_generation_spec(&task, &ctx).await.unwrap();
        assert!(spec.similar_files.is_empty(), "No similar files without LanceDb");
        assert!(spec.recommended_recipe.is_none(), "No recipe without LanceDb");
    }

    #[tokio::test]
    async fn test_is_llm_available_delegates_to_provider() {
        // MockLlm uses the default is_available() which returns true
        let (_, gimli, _tmp) = setup("").await;
        assert!(gimli.is_llm_available(), "MockLlm should report available (default trait impl)");
    }

    struct UnavailableLlm;

    #[async_trait]
    impl LlmProvider for UnavailableLlm {
        async fn complete(&self, _prompt: &str) -> anyhow::Result<String> {
            Err(anyhow::anyhow!("unavailable"))
        }
        async fn complete_with_system(&self, _sys: &str, _prompt: &str) -> anyhow::Result<String> {
            Err(anyhow::anyhow!("unavailable"))
        }
        fn is_available(&self) -> bool {
            false
        }
    }

    #[tokio::test]
    async fn test_is_llm_available_returns_false_for_unavailable_provider() {
        let tmp = TempDir::new().unwrap();
        let sqlite = Arc::new(SqliteDb::new(&tmp.path().join("t.db")).unwrap());
        let llm: Arc<dyn LlmProvider> = Arc::new(UnavailableLlm);
        let gimli = Gimli::new(llm, sqlite);
        assert!(!gimli.is_llm_available());
    }

    // -- Tests: extract_code_block --

    #[test]
    fn test_extract_code_block_normal_fenced() {
        let response = "Here's the code:\n```rust\nfn main() {}\n```\nDone.";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "fn main() {}");
    }

    #[test]
    fn test_extract_code_block_empty_fence_returns_empty() {
        let response = "```rust\n```";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "", "Empty fenced block should return empty string");
    }

    #[test]
    fn test_extract_code_block_no_fence_returns_raw() {
        let response = "fn main() {}";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "fn main() {}");
    }

    #[test]
    fn test_extract_code_block_with_surrounding_text() {
        let response = "Sure, here's the code:\n```\nline1\nline2\n```\nAll done!";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "line1\nline2");
    }

    #[test]
    fn test_extract_code_block_multiple_blocks_returns_first() {
        let response = "```\nfirst\n```\n```\nsecond\n```";
        let code = Gimli::extract_code_block(response);
        assert_eq!(code, "first");
    }

    // -- Tests: GenerationSpec serialization --

    #[test]
    fn test_generation_spec_serialization() {
        let spec = GenerationSpec {
            task_id: "T1".into(),
            target_file: "src/foo.rs".into(),
            task_type: TaskType::Create,
            description: "Create foo".into(),
            system_prompt: "You are an expert".into(),
            user_prompt: "## Task\n...".into(),
            language: "Rust".into(),
            constraint_count: 3,
            example_count: 2,
            prompt_tokens_estimate: 500,
            similar_files: vec![SimilarFile {
                file_path: "src/bar.rs".into(),
                summary: "bar file".into(),
                similarity_score: 0.85,
            }],
            recommended_recipe: Some("test_gen".into()),
        };

        let json = serde_json::to_value(&spec).unwrap();
        assert_eq!(json["task_id"], "T1");
        assert_eq!(json["similar_files"][0]["similarity_score"], 0.85);
        assert_eq!(json["recommended_recipe"], "test_gen");

        // Round-trip
        let deserialized: GenerationSpec = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized.task_id, spec.task_id);
        assert_eq!(deserialized.similar_files.len(), 1);
    }

    // -- Tests: batch code generation --

    #[tokio::test]
    async fn test_generate_code_batch_empty() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let result = gimli.generate_code_batch(&[], &[]).await.unwrap();
        assert!(result.is_empty());
    }

    #[tokio::test]
    async fn test_generate_code_batch_single_delegates_to_generate_code() {
        let code_response = "```rust\npub fn hello() -> &'static str { \"hi\" }\n```";
        let (sqlite, gimli, _tmp) = setup(code_response).await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let ctx = sample_context();

        let results = gimli.generate_code_batch(&[task], &[ctx]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].task_id, "task-g-001");
        assert!(results[0].code.contains("hello"));
    }

    #[tokio::test]
    async fn test_generate_code_batch_multi_task() {
        // Mock LLM returns a batch response with two task markers.
        let batch_response = "\
// === TASK: task-g-001 ===\n\
```rust\n\
pub fn auth() -> Result<()> { Ok(()) }\n\
```\n\
// === TASK: task-g-002 ===\n\
```rust\n\
pub fn logout() { }\n\
```";
        let (sqlite, gimli, _tmp) = setup(batch_response).await;
        seed_scroll_for_gimli(&sqlite).await;

        let task1 = sample_task();
        let mut task2 = sample_task();
        task2.id = "task-g-002".into();
        task2.target_file = "src/handlers/logout.rs".into();
        task2.description = "Create logout handler".into();
        sqlite.create_task(&task2).await.expect("task2");

        let ctx1 = sample_context();
        let ctx2 = sample_context();

        let results = gimli
            .generate_code_batch(&[task1, task2], &[ctx1, ctx2])
            .await
            .unwrap();
        assert_eq!(results.len(), 2);
        assert_eq!(results[0].task_id, "task-g-001");
        assert!(results[0].code.contains("auth"));
        assert_eq!(results[1].task_id, "task-g-002");
        assert!(results[1].code.contains("logout"));
    }

    #[tokio::test]
    async fn test_generate_code_batch_fallback_on_parse_failure() {
        // Mock LLM returns something that doesn't have task markers,
        // but individual generate_code will work since MockLlm returns
        // the same response each time (which gets extracted as a code block).
        let code_response = "```rust\npub fn fallback() {}\n```";
        let (sqlite, gimli, _tmp) = setup(code_response).await;
        seed_scroll_for_gimli(&sqlite).await;

        let task1 = sample_task();
        let mut task2 = sample_task();
        task2.id = "task-g-002".into();
        task2.target_file = "src/handlers/logout.rs".into();
        sqlite.create_task(&task2).await.expect("task2");

        let ctx1 = sample_context();
        let ctx2 = sample_context();

        // Batch parse will fail (no task markers), falls back to individual.
        let results = gimli
            .generate_code_batch(&[task1, task2], &[ctx1, ctx2])
            .await
            .unwrap();
        assert_eq!(results.len(), 2);
        // Both get the same fallback code.
        assert!(results[0].code.contains("fallback"));
        assert!(results[1].code.contains("fallback"));
    }

    #[test]
    fn test_parse_batch_response_valid() {
        let response = "\
// === TASK: TASK-001 ===\n\
```rust\n\
fn a() {}\n\
```\n\
// === TASK: TASK-002 ===\n\
```rust\n\
fn b() {}\n\
```";
        let tasks = vec![
            Task {
                id: "TASK-001".into(),
                scroll_id: "s".into(),
                description: "A".into(),
                target_file: "a.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
            Task {
                id: "TASK-002".into(),
                scroll_id: "s".into(),
                description: "B".into(),
                target_file: "b.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
        ];

        let result = Gimli::parse_batch_response(response, &tasks);
        assert!(result.is_some());
        let map = result.unwrap();
        assert_eq!(map.len(), 2);
        assert!(map["TASK-001"].contains("fn a()"));
        assert!(map["TASK-002"].contains("fn b()"));
    }

    #[test]
    fn test_parse_batch_response_missing_task() {
        let response = "// === TASK: TASK-001 ===\n```rust\nfn a() {}\n```";
        let tasks = vec![
            Task {
                id: "TASK-001".into(),
                scroll_id: "s".into(),
                description: "A".into(),
                target_file: "a.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
            Task {
                id: "TASK-002".into(),
                scroll_id: "s".into(),
                description: "B".into(),
                target_file: "b.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
        ];

        // Only one task found in response, but two expected → None.
        let result = Gimli::parse_batch_response(response, &tasks);
        assert!(result.is_none());
    }

    #[test]
    fn test_parse_batch_response_positional_no_markers() {
        // No task markers at all — just two fenced code blocks in order.
        let response = "\
Here is the code for both tasks:\n\
```rust\n\
fn first() {}\n\
```\n\
```rust\n\
fn second() {}\n\
```";
        let tasks = vec![
            Task {
                id: "TASK-aug-1739388000000".into(),
                scroll_id: "s".into(),
                description: "A".into(),
                target_file: "a.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
            Task {
                id: "TASK-aug-1739388000001".into(),
                scroll_id: "s".into(),
                description: "B".into(),
                target_file: "b.rs".into(),
                task_type: TaskType::Create,
                status: TaskStatus::Pending,
                dependencies: vec![],
                generated_code: None,
                diff: None,
                created_at: Utc::now(),
                group_label: None,
            },
        ];

        let result = Gimli::parse_batch_response(response, &tasks);
        assert!(result.is_some());
        let map = result.unwrap();
        assert_eq!(map.len(), 2);
        assert!(map["TASK-aug-1739388000000"].contains("fn first()"));
        assert!(map["TASK-aug-1739388000001"].contains("fn second()"));
    }

    #[test]
    fn test_extract_all_code_blocks() {
        let response = "\
Some preamble text.\n\
```rust\n\
fn alpha() {}\n\
```\n\
Some middle text.\n\
```python\n\
def beta(): pass\n\
```\n\
Trailing text.";
        let blocks = Gimli::extract_all_code_blocks(response);
        assert_eq!(blocks.len(), 2);
        assert!(blocks[0].contains("fn alpha()"));
        assert!(blocks[1].contains("def beta()"));
    }

    #[test]
    fn test_extract_all_code_blocks_preserves_empty_blocks() {
        // Empty blocks are preserved for positional fidelity in batch parsing.
        let response = "```\n\n```\n```rust\nfn real() {}\n```";
        let blocks = Gimli::extract_all_code_blocks(response);
        assert_eq!(blocks.len(), 2);
        assert!(blocks[0].trim().is_empty());
        assert!(blocks[1].contains("fn real()"));
    }

    // -- Tests: metadata consistency --

    #[tokio::test]
    async fn test_generate_code_metadata_uses_enriched_context() {
        // The enriched context should count examples from SearchOrchestrator too.
        // Without SearchOrchestrator wired, the enriched context = original context.
        let code_response = "```rust\npub async fn auth() -> Result<()> { Ok(()) }\n```";
        let (sqlite, gimli, _tmp) = setup(code_response).await;
        seed_scroll_for_gimli(&sqlite).await;
        let task = sample_task();
        let ctx = sample_context(); // has 1 pattern example

        let generated = gimli.generate_code(&task, &ctx).await.unwrap();

        assert_eq!(generated.metadata.example_count, 1);
        assert_eq!(generated.metadata.constraint_count, 2);
        assert!(generated.metadata.prompt_tokens_estimate > 0);
    }

    #[test]
    fn test_generation_spec_to_prompt_preserves_fields() {
        let spec = GenerationSpec {
            task_id: "TASK-042".into(),
            target_file: "src/handler.rs".into(),
            task_type: TaskType::Create,
            description: "Create handler".into(),
            system_prompt: "You are an expert Rust programmer".into(),
            user_prompt: "Generate a handler for...".into(),
            language: "Rust".into(),
            constraint_count: 2,
            example_count: 1,
            prompt_tokens_estimate: 500,
            similar_files: vec![],
            recommended_recipe: None,
        };

        let prompt = Gimli::generation_spec_to_prompt(&spec);

        assert_eq!(prompt.id, "gimli-gen-TASK-042");
        assert_eq!(prompt.agent, "Gimli");
        assert_eq!(prompt.task_id, Some("TASK-042".into()));
        assert_eq!(prompt.system_prompt, spec.system_prompt);
        assert_eq!(prompt.user_prompt, spec.user_prompt);
        assert!(prompt.description.contains("Rust"));
        assert!(prompt.description.contains("src/handler.rs"));
    }

    // -- Tests: build_prompt_with_group_context --

    #[tokio::test]
    async fn test_build_prompt_with_group_context_appends_siblings() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let mut task = sample_task();
        task.group_label = Some("auth-module".into());
        let context = sample_context();

        let sibling_with_code = Task {
            id: "task-g-002".into(),
            scroll_id: "SCROLL-001".into(),
            description: "Add auth middleware".into(),
            target_file: "src/middleware/auth.rs".into(),
            task_type: TaskType::Create,
            status: TaskStatus::Completed,
            dependencies: vec![],
            generated_code: Some("// middleware code".into()),
            diff: None,
            created_at: Utc::now(),
            group_label: Some("auth-module".into()),
        };
        let sibling_pending = Task {
            id: "task-g-003".into(),
            scroll_id: "SCROLL-001".into(),
            description: "Add auth tests".into(),
            target_file: "src/handlers/auth_test.rs".into(),
            task_type: TaskType::Test,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: None,
            diff: None,
            created_at: Utc::now(),
            group_label: Some("auth-module".into()),
        };

        let siblings: Vec<&Task> = vec![&sibling_with_code, &sibling_pending];
        let prompt = gimli.build_prompt_with_group_context(&task, &context, &siblings);

        assert!(prompt.contains("## Group Context (group: auth-module)"));
        assert!(prompt.contains("src/middleware/auth.rs"));
        assert!(prompt.contains("[has code]"));
        assert!(prompt.contains("src/handlers/auth_test.rs"));
        assert!(prompt.contains("[pending]"));
        assert!(prompt.contains("Ensure imports, types, and interfaces are compatible"));
    }

    #[tokio::test]
    async fn test_build_prompt_with_group_context_empty_siblings_no_section() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let task = sample_task();
        let context = sample_context();
        let prompt = gimli.build_prompt_with_group_context(&task, &context, &[]);
        assert!(!prompt.contains("## Group Context"));
    }

    // -- Tests: build_prompt for Delete and Test task types --

    #[tokio::test]
    async fn test_build_prompt_delete_task() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let mut task = sample_task();
        task.task_type = TaskType::Delete;
        let context = sample_context();
        let prompt = gimli.build_prompt(&task, &context);
        assert!(prompt.contains("should be deleted"));
        assert!(prompt.contains("// DELETE"));
    }

    #[tokio::test]
    async fn test_build_prompt_test_task() {
        let (_sqlite, gimli, _tmp) = setup("").await;
        let mut task = sample_task();
        task.task_type = TaskType::Test;
        let context = sample_context();
        let prompt = gimli.build_prompt(&task, &context);
        assert!(prompt.contains("Generate a COMPLETE test file"));
    }

    // -- Tests: validate_generation with ShouldNot constraint --

    #[tokio::test]
    async fn test_validate_generation_should_not_advisory() {
        let (_, gimli, _tmp) = setup("").await;

        let generated = GeneratedCode {
            task_id: "t-sn".into(),
            target_file: "test.rs".into(),
            code: "let mut counter = 0;".into(),
            diff: String::new(),
            language: "Rust".into(),
            metadata: GenerationMetadata {
                constraint_count: 1,
                example_count: 0,
                prompt_tokens_estimate: 0,
                generated_at: Utc::now().to_rfc3339(),
            },
        };

        let constraints = vec![Constraint {
            id: "c-should-not-mut".into(),
            constraint_type: ConstraintType::ShouldNot,
            description: "Avoid mutable state".into(),
            pattern: "mut".into(),
            source: ConstraintSource::UserSpecified,
        }];

        let results = gimli
            .validate_generation(&generated, &constraints)
            .await
            .unwrap();

        // ShouldNot is advisory — all results pass but a warning is issued
        assert!(results.iter().all(|r| r.passed));
        assert!(results.iter().any(|r| r.message.contains("SHOULD NOT advisory")));
    }
}
