//! Adapters opcionais para reduzir trabalho inutil no fluxo SDD.
//!
//! O modulo nao instala ferramentas nem cria indices. Ele detecta CodeGraph,
//! RTK e Caveman quando ja existem, registra fallback e fornece compactacao
//! operacional segura para traces/handoffs/memoria derivada.

use serde::Serialize;
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::domain::providers::OptimizationConfig;
use crate::runtime::redaction::redact_text;

pub const DEFAULT_IGNORED_PATHS: &[&str] = &[
    ".git",
    ".codegraph",
    ".sdd",
    ".sdd-execution",
    ".sdd-parallel",
    "target",
    "node_modules",
    "dist",
    "build",
    ".next",
    ".cache",
    "coverage",
    "tmp",
    "temp",
    "vendor",
];

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct OptimizationStatus {
    pub enabled: bool,
    pub codegraph: ToolStatus,
    pub rtk: ToolStatus,
    pub caveman: ToolStatus,
    pub ignored_paths: Vec<String>,
    pub loop_policy: ProductiveLoopPolicy,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ToolStatus {
    pub id: String,
    pub command: String,
    pub available: bool,
    pub version: Option<String>,
    pub indexed: Option<bool>,
    pub index_marker: Option<String>,
    pub mode: String,
    pub capabilities: Vec<String>,
    pub fallback: String,
    pub recommended_action: String,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct CodeGraphIndexResult {
    pub status: String,
    pub command: String,
    pub message: String,
}

pub struct CodeGraphAdapter<'a> {
    root: &'a Path,
    config: &'a OptimizationConfig,
}

impl<'a> CodeGraphAdapter<'a> {
    pub fn new(root: &'a Path, config: &'a OptimizationConfig) -> Self {
        Self { root, config }
    }

    pub fn status(&self) -> ToolStatus {
        codegraph_status(self.root, self.config)
    }

    pub fn context_command(&self, task: &str) -> String {
        let task = task.replace('"', "\\\"");
        format!(
            "{} context \"{}\" --path . --format markdown",
            self.config.codegraph.command, task
        )
    }

    pub fn affected_command(&self) -> String {
        format!(
            "git diff --name-only | {} affected --path . --stdin --quiet",
            self.config.codegraph.command
        )
    }

    pub fn auto_index(&self, dry_run: bool) -> CodeGraphIndexResult {
        auto_index_codegraph(self.root, self.config, dry_run)
    }
}

pub struct RtkAdapter<'a> {
    root: &'a Path,
    config: &'a OptimizationConfig,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct OptimizedCommandOutput {
    pub command: String,
    pub used_rtk: bool,
    pub status_code: Option<i32>,
    pub stdout: String,
    pub stderr: String,
    pub full_output_path: Option<String>,
}

impl<'a> RtkAdapter<'a> {
    pub fn new(root: &'a Path, config: &'a OptimizationConfig) -> Self {
        Self { root, config }
    }

    pub fn status(&self) -> ToolStatus {
        rtk_status(self.config)
    }

    pub fn run(&self, args: &[&str]) -> io::Result<OptimizedCommandOutput> {
        if args.is_empty() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "optimized command requires at least one argument",
            ));
        }
        let use_rtk = tool_mode_enabled(&self.config.rtk.enabled)
            && command_available(&self.config.rtk.command);
        let mut command = if use_rtk {
            let mut command = Command::new(&self.config.rtk.command);
            command.args(args);
            command
        } else {
            let exe = crate::runtime::platform::find_executable(args[0])
                .unwrap_or_else(|| PathBuf::from(args[0]));
            let mut command = Command::new(exe);
            command.args(&args[1..]);
            command
        };
        let output = command.current_dir(self.root).output()?;
        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        let rendered_command = if use_rtk {
            format!("{} {}", self.config.rtk.command, args.join(" "))
        } else {
            args.join(" ")
        };
        let full_output_path = if output.status.success() {
            None
        } else {
            persist_failed_command_output(self.root, &rendered_command, &stdout, &stderr)?
                .map(|path| path.to_string_lossy().replace('\\', "/"))
        };
        Ok(OptimizedCommandOutput {
            command: rendered_command,
            used_rtk: use_rtk,
            status_code: output.status.code(),
            stdout,
            stderr,
            full_output_path,
        })
    }
}

pub struct CavemanAdapter<'a> {
    config: &'a OptimizationConfig,
}

impl<'a> CavemanAdapter<'a> {
    pub fn new(config: &'a OptimizationConfig) -> Self {
        Self { config }
    }

    pub fn status(&self) -> ToolStatus {
        caveman_status(self.config)
    }

    pub fn compress(&self, kind: CompressionKind, input: &str) -> String {
        compress_operational_text(kind, input)
    }
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ProductiveLoopPolicy {
    pub required_progress: Vec<String>,
    pub stop_when_repeated: Vec<String>,
}

#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
pub struct LoopIterationEvidence {
    pub hypothesis: Option<String>,
    pub source: Option<String>,
    pub scope: Option<String>,
    pub evidence: Option<String>,
    pub error_signature: Option<String>,
    pub diff_signature: Option<String>,
    pub command: Option<String>,
    pub search: Option<String>,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct LoopDecision {
    pub continue_loop: bool,
    pub reason: String,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ContextHandoff {
    pub stage: String,
    pub task: Option<String>,
    pub strategy: String,
    pub artifact_sources: Vec<String>,
    pub probable_files: Vec<String>,
    pub suggested_tests: Vec<String>,
    pub fallback_used: String,
    pub out_of_scope_paths: Vec<String>,
    pub productive_loop_policy: ProductiveLoopPolicy,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompressionKind {
    Trace,
    Handoff,
    Memory,
}

impl CompressionKind {
    pub fn parse(value: &str) -> Option<Self> {
        match value.trim().to_ascii_lowercase().as_str() {
            "trace" => Some(Self::Trace),
            "handoff" => Some(Self::Handoff),
            "memory" | "memory-derived" | "memoria" | "memória" => Some(Self::Memory),
            _ => None,
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Trace => "trace",
            Self::Handoff => "handoff",
            Self::Memory => "memory-derived",
        }
    }
}

pub fn status(root: &Path, config: &OptimizationConfig) -> OptimizationStatus {
    let ignored_paths = if config.ignored_paths.is_empty() {
        ignored_path_list()
    } else {
        config.ignored_paths.clone()
    };
    OptimizationStatus {
        enabled: config.enabled,
        codegraph: codegraph_status(root, config),
        rtk: rtk_status(config),
        caveman: caveman_status(config),
        ignored_paths,
        loop_policy: productive_loop_policy(),
    }
}

pub fn ignored_path_list() -> Vec<String> {
    DEFAULT_IGNORED_PATHS
        .iter()
        .map(|item| (*item).to_string())
        .collect()
}

pub fn path_has_ignored_component(path: &Path) -> bool {
    path.components().any(|component| {
        matches!(
            component,
            Component::Normal(name)
                if DEFAULT_IGNORED_PATHS.contains(&name.to_string_lossy().as_ref())
        )
    })
}

pub fn build_context_handoff(
    root: &Path,
    stage: &str,
    task: Option<&str>,
    artifact_sources: Vec<String>,
    status: &OptimizationStatus,
) -> ContextHandoff {
    let probable_files = git_changed_files(root);
    let suggested_tests = suggested_tests(root, &probable_files, status);
    let strategy = if status.codegraph.available && status.codegraph.indexed == Some(true) {
        "codegraph-indexed".to_string()
    } else if !probable_files.is_empty() {
        "git-diff-focused".to_string()
    } else {
        "rg-focused-fallback".to_string()
    };
    let fallback_used = if strategy == "codegraph-indexed" {
        "CodeGraph indexado disponível; valide impacto com leitura/testes reais.".to_string()
    } else {
        "CodeGraph ausente ou não indexado; use git diff/status, manifests e rg delimitado."
            .to_string()
    };
    ContextHandoff {
        stage: stage.to_string(),
        task: task.map(str::to_string),
        strategy,
        artifact_sources,
        probable_files,
        suggested_tests,
        fallback_used,
        out_of_scope_paths: status.ignored_paths.clone(),
        productive_loop_policy: status.loop_policy.clone(),
    }
}

pub fn auto_index_codegraph(
    root: &Path,
    config: &OptimizationConfig,
    dry_run: bool,
) -> CodeGraphIndexResult {
    let command = config.codegraph.command.clone();
    let command_line = format!("{command} init -i .");
    if !config.enabled {
        return codegraph_index_result(
            "disabled",
            command_line,
            "optimization disabled; skipping CodeGraph auto-index.",
        );
    }
    if !config.codegraph.auto_index {
        return codegraph_index_result(
            "disabled",
            command_line,
            "optimization.codegraph.auto_index=false; skipping CodeGraph auto-index.",
        );
    }
    if root.join(&config.codegraph.index_marker).exists() {
        return codegraph_index_result(
            "already-indexed",
            command_line,
            "CodeGraph index already exists; skipping `codegraph init -i .`.",
        );
    }
    let Some(executable) = crate::runtime::platform::find_executable(&command) else {
        return codegraph_index_result(
            "missing",
            command_line,
            "CodeGraph not found in PATH; SDD init continues with rg/git fallback.",
        );
    };
    if dry_run {
        return codegraph_index_result("dry-run", command_line, "dry-run codegraph init -i .");
    }
    match Command::new(executable)
        .current_dir(root)
        .args(["init", "-i", "."])
        .output()
    {
        Ok(output) if output.status.success() => {
            codegraph_index_result("indexed", command_line, "ran codegraph init -i .")
        }
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let stdout = String::from_utf8_lossy(&output.stdout);
            let detail = first_non_empty_line(&stderr)
                .or_else(|| first_non_empty_line(&stdout))
                .unwrap_or_else(|| "unknown CodeGraph error".to_string());
            codegraph_index_result(
                "failed",
                command_line,
                format!("codegraph init -i . failed; SDD init continues. {detail}"),
            )
        }
        Err(err) => codegraph_index_result(
            "failed",
            command_line,
            format!("codegraph init -i . failed to start; SDD init continues. {err}"),
        ),
    }
}

pub fn render_context_handoff(handoff: &ContextHandoff) -> String {
    let mut out = String::new();
    out.push_str("## Context Handoff\n\n");
    out.push_str(&format!("- Stage: `{}`\n", handoff.stage));
    if let Some(task) = handoff.task.as_deref() {
        out.push_str(&format!("- Task: `{}`\n", task));
    }
    out.push_str(&format!("- Estratégia: `{}`\n", handoff.strategy));
    out.push_str(&format!("- Fallback usado: {}\n", handoff.fallback_used));
    push_list(
        &mut out,
        "Artefatos base",
        &handoff.artifact_sources,
        "nenhum",
    );
    push_list(
        &mut out,
        "Arquivos prováveis",
        &handoff.probable_files,
        "nenhum arquivo alterado detectado por Git",
    );
    push_list(
        &mut out,
        "Testes sugeridos",
        &handoff.suggested_tests,
        "descobrir pelo manifesto do projeto",
    );
    push_list(
        &mut out,
        "Fora de escopo por padrão",
        &handoff.out_of_scope_paths,
        "nenhum",
    );
    out.push_str("- Loop produtivo: continue somente com nova hipótese, nova fonte, escopo menor ou evidência nova.\n");
    out.push_str(
        "- Parar se repetir o mesmo erro, diff, comando ou busca sem informação nova.\n\n",
    );
    out
}

pub fn evaluate_productive_loop(
    history: &[LoopIterationEvidence],
    current: &LoopIterationEvidence,
) -> LoopDecision {
    if has_new_progress(history, current) {
        return LoopDecision {
            continue_loop: true,
            reason: "nova hipótese, fonte, escopo ou evidência registrada".to_string(),
        };
    }
    if repeated_signature(history, current, |item| item.error_signature.as_deref()) {
        return stop_loop("mesmo erro repetido sem evidência nova");
    }
    if repeated_signature(history, current, |item| item.diff_signature.as_deref()) {
        return stop_loop("mesmo diff repetido sem evidência nova");
    }
    if repeated_signature(history, current, |item| item.command.as_deref()) {
        return stop_loop("mesmo comando repetido sem evidência nova");
    }
    if repeated_signature(history, current, |item| item.search.as_deref()) {
        return stop_loop("mesma busca repetida sem evidência nova");
    }
    stop_loop("iteração sem progresso observável")
}

pub fn compress_operational_text(kind: CompressionKind, input: &str) -> String {
    let mut seen = BTreeSet::new();
    let mut kept = Vec::new();
    let max_lines = match kind {
        CompressionKind::Trace => 24,
        CompressionKind::Handoff => 36,
        CompressionKind::Memory => 48,
    };
    for line in input.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let redacted = redact_text(trimmed);
        let normalized = normalize_operational_line(&redacted);
        if seen.insert(normalized) {
            kept.push(redacted);
        }
        if kept.len() >= max_lines {
            break;
        }
    }
    if kept.is_empty() {
        return String::new();
    }
    let mut out = format!("# Caveman {} summary\n\n", kind.as_str());
    for line in kept {
        out.push_str("- ");
        out.push_str(&line);
        out.push('\n');
    }
    out
}

fn has_new_progress(history: &[LoopIterationEvidence], current: &LoopIterationEvidence) -> bool {
    [
        current.hypothesis.as_deref(),
        current.source.as_deref(),
        current.scope.as_deref(),
        current.evidence.as_deref(),
    ]
    .into_iter()
    .flatten()
    .any(|candidate| {
        let candidate = candidate.trim();
        !candidate.is_empty()
            && !history.iter().any(|item| {
                [
                    item.hypothesis.as_deref(),
                    item.source.as_deref(),
                    item.scope.as_deref(),
                    item.evidence.as_deref(),
                ]
                .into_iter()
                .flatten()
                .any(|seen| seen.trim() == candidate)
            })
    })
}

fn repeated_signature<F>(
    history: &[LoopIterationEvidence],
    current: &LoopIterationEvidence,
    get: F,
) -> bool
where
    F: Fn(&LoopIterationEvidence) -> Option<&str>,
{
    let Some(value) = get(current)
        .map(str::trim)
        .filter(|value| !value.is_empty())
    else {
        return false;
    };
    history
        .iter()
        .any(|item| get(item).map(str::trim) == Some(value))
}

fn stop_loop(reason: &str) -> LoopDecision {
    LoopDecision {
        continue_loop: false,
        reason: reason.to_string(),
    }
}

fn codegraph_index_result(
    status: impl Into<String>,
    command: impl Into<String>,
    message: impl Into<String>,
) -> CodeGraphIndexResult {
    CodeGraphIndexResult {
        status: status.into(),
        command: command.into(),
        message: message.into(),
    }
}

fn tool_mode_enabled(value: &str) -> bool {
    !matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "false" | "off" | "disabled" | "never"
    )
}

fn persist_failed_command_output(
    root: &Path,
    command: &str,
    stdout: &str,
    stderr: &str,
) -> io::Result<Option<PathBuf>> {
    let rel_dir = PathBuf::from(".sdd/optimization/rtk-failures");
    let dir = root.join(&rel_dir);
    fs::create_dir_all(&dir)?;
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    let slug = command
        .chars()
        .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
        .collect::<String>()
        .trim_matches('-')
        .chars()
        .take(48)
        .collect::<String>();
    let filename = if slug.is_empty() {
        format!("{timestamp}-command.log")
    } else {
        format!("{timestamp}-{slug}.log")
    };
    let rel_path = rel_dir.join(filename);
    let path = root.join(&rel_path);
    let mut body = String::new();
    body.push_str(&format!("$ {command}\n\n"));
    body.push_str("## stdout\n\n");
    body.push_str(stdout);
    if !stdout.ends_with('\n') {
        body.push('\n');
    }
    body.push_str("\n## stderr\n\n");
    body.push_str(stderr);
    if !stderr.ends_with('\n') {
        body.push('\n');
    }
    fs::write(&path, body)?;
    Ok(Some(rel_path))
}

fn codegraph_status(root: &Path, config: &OptimizationConfig) -> ToolStatus {
    let command = config.codegraph.command.as_str();
    let available = command_available(command);
    let version = if available {
        command_stdout(command, &["--version"])
    } else {
        None
    };
    let help = if available {
        command_stdout(command, &["--help"]).unwrap_or_default()
    } else {
        String::new()
    };
    let capabilities = codegraph_capabilities(&help);
    let marker = config.codegraph.index_marker.clone();
    let indexed = Some(root.join(&marker).exists());
    let recommended_action = if !available {
        "Instale CodeGraph para acelerar contexto; fallback rg/git ativo.".to_string()
    } else if indexed == Some(false) {
        "Rode `sdd init` ou `codegraph init -i .` para habilitar índice.".to_string()
    } else {
        "Use `codegraph context`, `query` e `affected` antes de leitura ampla.".to_string()
    };
    ToolStatus {
        id: "codegraph".to_string(),
        command: command.to_string(),
        available,
        version,
        indexed,
        index_marker: Some(marker),
        mode: config.codegraph.mode.clone(),
        capabilities,
        fallback: "git diff/status, manifests, rg --files e rg delimitado".to_string(),
        recommended_action,
    }
}

fn rtk_status(config: &OptimizationConfig) -> ToolStatus {
    let command = config.rtk.command.as_str();
    let available = command_available(command);
    let version = if available {
        command_stdout(command, &["--version"])
    } else {
        None
    };
    ToolStatus {
        id: "rtk".to_string(),
        command: command.to_string(),
        available,
        version,
        indexed: None,
        index_marker: None,
        mode: config.rtk.enabled.clone(),
        capabilities: vec!["command-proxy".to_string(), "token-savings".to_string()],
        fallback: "executar comando bruto e limitar stdout/stderr no caller".to_string(),
        recommended_action: if available {
            "Use RTK para comandos longos/ruidosos; use `rtk proxy` para shell complexo."
                .to_string()
        } else {
            "RTK ausente; execute comandos diretos com limites de output.".to_string()
        },
    }
}

fn caveman_status(config: &OptimizationConfig) -> ToolStatus {
    let command = config.caveman.command.as_str();
    let available = command_available(command);
    let version = if available {
        command_stdout(command, &["--version"]).or_else(|| command_stdout(command, &["--help"]))
    } else {
        None
    };
    ToolStatus {
        id: "caveman".to_string(),
        command: command.to_string(),
        available,
        version: version.map(|value| first_line(&value)),
        indexed: None,
        index_marker: None,
        mode: config.caveman.enabled.clone(),
        capabilities: vec![
            "operational-compression".to_string(),
            "trace-handoff-memory".to_string(),
        ],
        fallback: "compressão determinística local preservando paths, comandos, IDs e hashes"
            .to_string(),
        recommended_action: if available {
            "Use Caveman apenas para traces, handoffs e memória derivada.".to_string()
        } else {
            "Caveman ausente; usando compactação operacional embutida.".to_string()
        },
    }
}

fn codegraph_capabilities(help: &str) -> Vec<String> {
    let mut out = Vec::new();
    for capability in [
        "init", "index", "sync", "status", "query", "files", "context", "affected",
    ] {
        if help.contains(&format!("{capability} ")) || help.contains(&format!("  {capability}")) {
            out.push(capability.to_string());
        }
    }
    out
}

fn git_changed_files(root: &Path) -> Vec<String> {
    if !root.join(".git").exists() {
        return Vec::new();
    }
    let mut files = BTreeSet::new();
    if let Some(output) = command_stdout_in(root, "git", &["diff", "--name-only"]) {
        for line in output.lines() {
            push_probable_file(&mut files, line);
        }
    }
    if let Some(output) = command_stdout_in(root, "git", &["status", "--short"]) {
        for line in output.lines() {
            let path = line.get(3..).unwrap_or(line).trim();
            let path = path.split(" -> ").last().unwrap_or(path);
            push_probable_file(&mut files, path);
        }
    }
    files.into_iter().take(30).collect()
}

fn suggested_tests(root: &Path, files: &[String], status: &OptimizationStatus) -> Vec<String> {
    if !files.is_empty() && status.codegraph.available && status.codegraph.indexed == Some(true) {
        return vec!["git diff --name-only | codegraph affected --stdin --quiet".to_string()];
    }
    let mut tests = Vec::new();
    if root.join("Cargo.toml").exists() {
        tests.push("cargo test".to_string());
    }
    if root.join("package.json").exists() {
        tests.push("npm test".to_string());
    }
    if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
        tests.push("pytest".to_string());
    }
    if tests.is_empty() {
        tests.push("descobrir comando de teste via manifests/docs".to_string());
    }
    tests
}

fn productive_loop_policy() -> ProductiveLoopPolicy {
    ProductiveLoopPolicy {
        required_progress: vec![
            "nova hipótese testada".to_string(),
            "nova fonte lida".to_string(),
            "escopo reduzido".to_string(),
            "evidência nova produzida".to_string(),
        ],
        stop_when_repeated: vec![
            "mesmo erro".to_string(),
            "mesmo diff".to_string(),
            "mesmo comando".to_string(),
            "mesma busca sem informação nova".to_string(),
        ],
    }
}

fn push_list(out: &mut String, label: &str, items: &[String], empty: &str) {
    if items.is_empty() {
        out.push_str(&format!("- {label}: {empty}\n"));
    } else {
        out.push_str(&format!("- {label}: {}\n", items.join(", ")));
    }
}

fn push_probable_file(files: &mut BTreeSet<String>, value: &str) {
    let value = value.trim();
    if value.is_empty() {
        return;
    }
    let path = Path::new(value);
    if path_has_ignored_component(path) {
        return;
    }
    files.insert(value.replace('\\', "/"));
}

fn normalize_operational_line(input: &str) -> String {
    input
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .to_ascii_lowercase()
}

fn command_available(command: &str) -> bool {
    crate::runtime::platform::find_executable(command).is_some()
}

fn command_stdout(command: &str, args: &[&str]) -> Option<String> {
    let exe = crate::runtime::platform::find_executable(command)
        .unwrap_or_else(|| PathBuf::from(command));
    let output = Command::new(exe).args(args).output().ok()?;
    if output.status.success() {
        Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        if stderr.is_empty() {
            None
        } else {
            Some(stderr)
        }
    }
}

fn command_stdout_in(root: &Path, command: &str, args: &[&str]) -> Option<String> {
    let exe = crate::runtime::platform::find_executable(command)
        .unwrap_or_else(|| PathBuf::from(command));
    let output = Command::new(exe)
        .current_dir(root)
        .args(args)
        .output()
        .ok()?;
    if output.status.success() {
        Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        None
    }
}

fn first_line(input: &str) -> String {
    input.lines().next().unwrap_or(input).trim().to_string()
}

fn first_non_empty_line(input: &str) -> Option<String> {
    input
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty())
        .map(str::to_string)
}

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

    #[test]
    fn codegraph_help_detection_tracks_actual_commands() {
        let help =
            "Commands:\n  init\n  index\n  status\n  query\n  files\n  context\n  affected\n";
        let caps = codegraph_capabilities(help);
        assert!(caps.contains(&"context".to_string()));
        assert!(caps.contains(&"affected".to_string()));
        assert!(!caps.contains(&"impact".to_string()));
    }

    #[test]
    fn ignored_path_components_are_centralized() {
        assert!(path_has_ignored_component(Path::new("target/debug/app")));
        assert!(path_has_ignored_component(Path::new(
            "node_modules/pkg/index.js"
        )));
        assert!(!path_has_ignored_component(Path::new("src/main.rs")));
    }

    #[test]
    fn caveman_compression_preserves_operational_identifiers() {
        let input = "cmd: cargo test\ncmd: cargo test\nfile: src/main.rs\nTask T-01 OK\nhash abc123def456\n";
        let out = compress_operational_text(CompressionKind::Trace, input);
        assert!(out.contains("cargo test"));
        assert!(out.contains("src/main.rs"));
        assert!(out.contains("T-01"));
        assert!(out.contains("abc123def456"));
        assert_eq!(out.matches("cargo test").count(), 1);
    }

    #[test]
    fn productive_loop_continues_only_with_new_evidence() {
        let first = LoopIterationEvidence {
            hypothesis: Some("falha vem do parser".to_string()),
            evidence: Some("teste parse_invalid reproduz".to_string()),
            command: Some("cargo test parse_invalid".to_string()),
            error_signature: Some("E_PARSE".to_string()),
            ..LoopIterationEvidence::default()
        };
        let decision = evaluate_productive_loop(&[], &first);
        assert!(decision.continue_loop);

        let repeated = LoopIterationEvidence {
            command: Some("cargo test parse_invalid".to_string()),
            error_signature: Some("E_PARSE".to_string()),
            ..LoopIterationEvidence::default()
        };
        let decision = evaluate_productive_loop(&[first], &repeated);
        assert!(!decision.continue_loop);
        assert!(decision.reason.contains("mesmo erro"));
    }

    #[test]
    fn rtk_adapter_persists_full_output_path_on_failure() {
        let dir = tempdir().unwrap();
        let root = dir.path();
        let mut config = OptimizationConfig::default();
        config.rtk.enabled = "disabled".to_string();
        let adapter = RtkAdapter::new(root, &config);
        let output = adapter
            .run(&["sh", "-c", "printf 'raw-out'; printf 'raw-err' >&2; exit 7"])
            .unwrap();
        assert_eq!(output.status_code, Some(7));
        assert!(!output.used_rtk);
        let path = output.full_output_path.expect("failure log path");
        let body = std::fs::read_to_string(root.join(path)).unwrap();
        assert!(body.contains("raw-out"));
        assert!(body.contains("raw-err"));
    }

    #[test]
    fn context_handoff_uses_git_changes_without_wide_search() {
        let dir = tempdir().unwrap();
        let root = dir.path();
        std::fs::write(
            root.join("Cargo.toml"),
            "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
        )
        .unwrap();
        std::fs::create_dir_all(root.join("src")).unwrap();
        std::fs::write(root.join("src/lib.rs"), "pub fn a() {}\n").unwrap();
        Command::new("git")
            .args(["init"])
            .current_dir(root)
            .output()
            .unwrap();
        Command::new("git")
            .args(["add", "."])
            .current_dir(root)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(root)
            .env("GIT_AUTHOR_NAME", "t")
            .env("GIT_AUTHOR_EMAIL", "t@example.com")
            .env("GIT_COMMITTER_NAME", "t")
            .env("GIT_COMMITTER_EMAIL", "t@example.com")
            .output()
            .unwrap();
        std::fs::write(root.join("src/lib.rs"), "pub fn a() { let _x = 1; }\n").unwrap();
        let status = OptimizationStatus {
            enabled: true,
            codegraph: ToolStatus {
                id: "codegraph".to_string(),
                command: "codegraph".to_string(),
                available: false,
                version: None,
                indexed: Some(false),
                index_marker: Some(".codegraph/".to_string()),
                mode: "adaptive".to_string(),
                capabilities: Vec::new(),
                fallback: "rg".to_string(),
                recommended_action: "fallback".to_string(),
            },
            rtk: rtk_status(&OptimizationConfig::default()),
            caveman: caveman_status(&OptimizationConfig::default()),
            ignored_paths: ignored_path_list(),
            loop_policy: productive_loop_policy(),
        };
        let handoff = build_context_handoff(
            root,
            "execution",
            Some("T-01"),
            vec!["docs/x/04-tasks.md".to_string()],
            &status,
        );
        assert_eq!(handoff.strategy, "git-diff-focused");
        assert!(handoff.probable_files.contains(&"src/lib.rs".to_string()));
        assert!(handoff.suggested_tests.contains(&"cargo test".to_string()));
    }
}
