//! Execução real de tasks no TUI: parser de backlog, diff por snapshot e
//! composição do artefato final de Execution.

use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::{Component, Path};

use similar::TextDiff;
use walkdir::WalkDir;

use super::runner::StageResult;
use super::stage::Stage;

const MAX_TRACKED_FILE_BYTES: u64 = 512 * 1024;
const MAX_PATCH_CHARS_PER_TASK: usize = 12_000;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecutionTask {
    pub id: String,
    pub title: String,
    pub body: String,
    pub dependencies: Vec<String>,
    pub independent: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TaskRunStatus {
    Pending,
    Running,
    Succeeded,
    Failed,
    Skipped,
}

impl TaskRunStatus {
    pub fn label(self) -> &'static str {
        match self {
            TaskRunStatus::Pending => "Aguardando",
            TaskRunStatus::Running => "Em execução",
            TaskRunStatus::Succeeded => "Concluída",
            TaskRunStatus::Failed => "Erro",
            TaskRunStatus::Skipped => "Pulada",
        }
    }

    pub fn marker(self) -> &'static str {
        match self {
            TaskRunStatus::Pending => "○",
            TaskRunStatus::Running => "●",
            TaskRunStatus::Succeeded => "✓",
            TaskRunStatus::Failed => "✗",
            TaskRunStatus::Skipped => "⊘",
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DiffReport {
    pub changed_files: Vec<String>,
    pub summary: String,
    pub patch: String,
}

impl DiffReport {
    pub fn is_empty(&self) -> bool {
        self.changed_files.is_empty()
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WorkspaceSnapshot {
    files: BTreeMap<String, TrackedFile>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum TrackedFile {
    Text(String),
    BinaryOrLarge,
}

#[derive(Clone, Debug)]
pub struct CompletedTask {
    pub task: ExecutionTask,
    pub status: TaskRunStatus,
    pub report: String,
    pub diff: DiffReport,
    pub result: Option<StageResult>,
    pub trace: Vec<String>,
}

/// Planeja as tasks de execução a partir do Markdown canônico de `04-tasks.md`.
/// Aceita tanto a tabela legada `T-01` quanto seções `### TSK-01 — ...`.
pub fn parse_execution_tasks(md: &str) -> Vec<ExecutionTask> {
    let independent_ids = extract_independent_ids(md);
    let section_tasks = parse_section_tasks(md, &independent_ids);
    let table_tasks = parse_backlog_table(md, &independent_ids);
    if !table_tasks.is_empty() {
        return merge_table_and_section_tasks(table_tasks, section_tasks);
    }
    section_tasks
}

fn extract_independent_ids(md: &str) -> HashSet<String> {
    let mut ids = HashSet::new();
    let mut in_section = false;

    for line in md.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('#') {
            let heading = trimmed.trim_start_matches('#').trim().to_lowercase();
            in_section = heading.contains("independente") || heading.contains("paralel");
            continue;
        }
        if in_section {
            for id in extract_task_ids(trimmed) {
                ids.insert(id);
            }
        }
    }
    ids
}

fn parse_backlog_table(md: &str, independent_ids: &HashSet<String>) -> Vec<ExecutionTask> {
    let mut tasks = Vec::new();
    let mut in_backlog = false;

    for line in md.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('#') {
            let heading = trimmed.trim_start_matches('#').trim().to_lowercase();
            in_backlog = heading == "backlog";
            continue;
        }
        if !in_backlog || !trimmed.starts_with('|') {
            continue;
        }

        let cols = trimmed
            .split('|')
            .map(str::trim)
            .filter(|cell| !cell.is_empty())
            .collect::<Vec<_>>();
        if cols.len() < 2 {
            continue;
        }
        let Some((id, _)) = task_id_at_start(cols[0]) else {
            continue;
        };
        let title = cols[1].trim().to_string();
        if title.is_empty() {
            continue;
        }
        let deps = cols.get(3).copied().unwrap_or_default();
        let dependencies = extract_task_ids(deps);
        let independent = independent_ids.contains(&id) || has_no_declared_dependency(deps);
        tasks.push(ExecutionTask {
            id,
            title,
            body: String::new(),
            dependencies,
            independent,
        });
    }

    tasks
}

fn parse_section_tasks(md: &str, independent_ids: &HashSet<String>) -> Vec<ExecutionTask> {
    let mut tasks = Vec::new();
    let mut in_backlog = false;
    let mut current: Option<ExecutionTask> = None;

    for line in md.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("## ") {
            if let Some(task) = current.take() {
                tasks.push(finalize_section_task(task, independent_ids));
            }
            let heading = trimmed.trim_start_matches('#').trim().to_lowercase();
            in_backlog = is_task_detail_section(&heading);
            continue;
        }

        if !in_backlog && current.is_none() {
            continue;
        }

        if trimmed.starts_with('#') {
            if let Some((id, title)) = parse_task_heading(trimmed) {
                if let Some(task) = current.take() {
                    tasks.push(finalize_section_task(task, independent_ids));
                }
                current = Some(ExecutionTask {
                    id,
                    title,
                    body: String::new(),
                    dependencies: Vec::new(),
                    independent: false,
                });
                continue;
            }
        }

        if let Some(task) = current.as_mut() {
            if let Some(value) = trimmed.strip_prefix("- Dependências:") {
                task.dependencies.extend(extract_task_ids(value));
            }
            task.body.push_str(line);
            task.body.push('\n');
        }
    }

    if let Some(task) = current.take() {
        tasks.push(finalize_section_task(task, independent_ids));
    }

    tasks
}

fn is_task_detail_section(heading: &str) -> bool {
    matches!(
        heading,
        "backlog" | "prompts agent" | "prompts de execução" | "prompts de execucao"
    )
}

fn merge_table_and_section_tasks(
    mut table_tasks: Vec<ExecutionTask>,
    section_tasks: Vec<ExecutionTask>,
) -> Vec<ExecutionTask> {
    let details_by_id: BTreeMap<String, ExecutionTask> = section_tasks
        .into_iter()
        .map(|task| (task.id.clone(), task))
        .collect();

    for task in &mut table_tasks {
        let Some(detail) = details_by_id.get(&task.id) else {
            continue;
        };
        if !detail.body.trim().is_empty() {
            task.body = detail.body.clone();
        }
        let mut seen: BTreeSet<String> = task.dependencies.iter().cloned().collect();
        for dep in &detail.dependencies {
            if seen.insert(dep.clone()) {
                task.dependencies.push(dep.clone());
            }
        }
        task.independent = task.independent || (task.dependencies.is_empty() && detail.independent);
    }

    table_tasks
}

fn finalize_section_task(
    mut task: ExecutionTask,
    independent_ids: &HashSet<String>,
) -> ExecutionTask {
    task.body = task.body.trim().to_string();
    task.independent = independent_ids.contains(&task.id) || task.dependencies.is_empty();
    task
}

fn parse_task_heading(line: &str) -> Option<(String, String)> {
    let heading = line.trim_start_matches('#').trim();
    let (id, end) = task_id_at_start(heading)?;
    let title = heading[end..]
        .trim()
        .trim_start_matches(['—', '-', ':', '–'])
        .trim()
        .to_string();
    if title.is_empty() {
        None
    } else {
        Some((id, title))
    }
}

fn task_id_at_start(input: &str) -> Option<(String, usize)> {
    let trimmed = input.trim_start();
    let offset = input.len().saturating_sub(trimmed.len());
    for prefix in ["TSK-", "T-"] {
        if !trimmed.starts_with(prefix) {
            continue;
        }
        let digits: String = trimmed[prefix.len()..]
            .chars()
            .take_while(|ch| ch.is_ascii_digit())
            .collect();
        if digits.is_empty() {
            continue;
        }
        let end = offset + prefix.len() + digits.len();
        return Some((format!("{prefix}{digits}"), end));
    }
    None
}

fn extract_task_ids(input: &str) -> Vec<String> {
    let mut ids = Vec::new();
    let mut seen = HashSet::new();
    let chars = input.char_indices().collect::<Vec<_>>();
    for (idx, _) in &chars {
        let tail = &input[*idx..];
        if let Some((id, _)) = task_id_at_start(tail) {
            if seen.insert(id.clone()) {
                ids.push(id);
            }
        }
    }
    ids
}

fn has_no_declared_dependency(deps: &str) -> bool {
    let normalized = deps.trim();
    normalized.is_empty() || matches!(normalized, "—" | "-" | "–" | "Nenhuma" | "nenhuma")
}

pub fn capture_workspace_snapshot(
    root: &Path,
    artifact_dir_to_exclude: Option<&Path>,
) -> std::io::Result<WorkspaceSnapshot> {
    let mut files = BTreeMap::new();
    let artifact_dir = artifact_dir_to_exclude.and_then(|path| path.canonicalize().ok());
    let root_canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());

    for entry in WalkDir::new(root)
        .follow_links(false)
        .into_iter()
        .filter_entry(|entry| {
            !should_skip_entry(entry.path(), &root_canonical, artifact_dir.as_deref())
        })
    {
        let entry = entry?;
        if !entry.file_type().is_file() {
            continue;
        }
        let metadata = entry.metadata()?;
        let rel = match entry.path().strip_prefix(root) {
            Ok(rel) => normalize_path(rel),
            Err(_) => continue,
        };
        if metadata.len() > MAX_TRACKED_FILE_BYTES {
            files.insert(rel, TrackedFile::BinaryOrLarge);
            continue;
        }
        let bytes = std::fs::read(entry.path())?;
        match String::from_utf8(bytes) {
            Ok(text) => {
                files.insert(rel, TrackedFile::Text(text));
            }
            Err(_) => {
                files.insert(rel, TrackedFile::BinaryOrLarge);
            }
        }
    }

    Ok(WorkspaceSnapshot { files })
}

fn should_skip_entry(path: &Path, root: &Path, artifact_dir: Option<&Path>) -> bool {
    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
    if artifact_dir.is_some_and(|artifact_dir| canonical.starts_with(artifact_dir)) {
        return true;
    }
    let rel = canonical.strip_prefix(root).unwrap_or(&canonical);
    crate::runtime::optimization::path_has_ignored_component(rel)
}

fn normalize_path(path: &Path) -> String {
    path.components()
        .filter_map(|component| match component {
            Component::Normal(value) => Some(value.to_string_lossy().to_string()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/")
}

pub fn diff_snapshots(before: &WorkspaceSnapshot, after: &WorkspaceSnapshot) -> DiffReport {
    let mut keys = BTreeSet::new();
    keys.extend(before.files.keys().cloned());
    keys.extend(after.files.keys().cloned());

    let mut changed_files = Vec::new();
    let mut summary = Vec::new();
    let mut patch = String::new();

    for path in keys {
        let old = before.files.get(&path);
        let new = after.files.get(&path);
        if old == new {
            continue;
        }
        changed_files.push(path.clone());
        match (old, new) {
            (None, Some(TrackedFile::Text(new_text))) => {
                summary.push(format!("A {path}"));
                append_unified_diff(&mut patch, &path, "", new_text);
            }
            (Some(TrackedFile::Text(old_text)), None) => {
                summary.push(format!("D {path}"));
                append_unified_diff(&mut patch, &path, old_text, "");
            }
            (Some(TrackedFile::Text(old_text)), Some(TrackedFile::Text(new_text))) => {
                summary.push(format!("M {path}"));
                append_unified_diff(&mut patch, &path, old_text, new_text);
            }
            (None, Some(TrackedFile::BinaryOrLarge)) => {
                summary.push(format!("A {path} (binário/grande)"));
                patch.push_str(&format!(
                    "diff -- {path}\nArquivo binário ou grande adicionado.\n\n"
                ));
            }
            (Some(TrackedFile::BinaryOrLarge), None) => {
                summary.push(format!("D {path} (binário/grande)"));
                patch.push_str(&format!(
                    "diff -- {path}\nArquivo binário ou grande removido.\n\n"
                ));
            }
            _ => {
                summary.push(format!("M {path} (binário/grande)"));
                patch.push_str(&format!(
                    "diff -- {path}\nArquivo binário ou grande alterado.\n\n"
                ));
            }
        }
    }

    DiffReport {
        changed_files,
        summary: summary.join("\n"),
        patch,
    }
}

fn append_unified_diff(out: &mut String, path: &str, old_text: &str, new_text: &str) {
    let diff = TextDiff::from_lines(old_text, new_text);
    let rendered = diff
        .unified_diff()
        .header(&format!("a/{path}"), &format!("b/{path}"))
        .to_string();
    if rendered.trim().is_empty() {
        return;
    }
    out.push_str(&rendered);
    if !out.ends_with('\n') {
        out.push('\n');
    }
    out.push('\n');
}

pub fn git_summary(root: &Path) -> Option<String> {
    if !root.join(".git").exists() {
        return None;
    }
    let status = std::process::Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("status")
        .arg("--short")
        .output()
        .ok()?;
    if !status.status.success() {
        return None;
    }
    let diff_stat = std::process::Command::new("git")
        .arg("-C")
        .arg(root)
        .arg("diff")
        .arg("--stat")
        .output()
        .ok();
    let mut out = String::new();
    let status_text = String::from_utf8_lossy(&status.stdout);
    if status_text.trim().is_empty() {
        out.push_str("git status --short: sem alterações rastreadas.\n");
    } else {
        out.push_str("git status --short:\n");
        out.push_str(status_text.trim());
        out.push('\n');
    }
    if let Some(diff_stat) = diff_stat.filter(|output| output.status.success()) {
        let stat = String::from_utf8_lossy(&diff_stat.stdout);
        if !stat.trim().is_empty() {
            out.push_str("\ngit diff --stat:\n");
            out.push_str(stat.trim());
            out.push('\n');
        }
    }
    Some(out)
}

pub fn compose_execution_artifact(
    orchestration_name: &str,
    tasks: &[CompletedTask],
    git_summary: Option<&str>,
    partial: bool,
) -> String {
    let mut out = String::new();
    out.push_str(&format!("# Execution - {orchestration_name}\n\n"));
    out.push_str("## Rastreabilidade\n\n");
    out.push_str(&format!("- Orquestração: {orchestration_name}\n"));
    out.push_str(&format!("- Stage: `{}`\n", Stage::Execution.key()));
    out.push_str("- Modo TUI: execução real task-by-task com diff por task\n");
    out.push_str("- Fonte das tasks: `04-tasks.md`\n");
    out.push_str(&format!("- Total de tasks planejadas: {}\n", tasks.len()));
    out.push_str(&format!(
        "- Estado do artefato: {}\n\n",
        if partial {
            "execução parcial documentada"
        } else {
            "execução concluída e documentada"
        }
    ));

    out.push_str("## Tarefa\n\n");
    out.push_str("| Task | Status | Arquivos alterados |\n");
    out.push_str("|------|--------|--------------------|\n");
    for task in tasks {
        let files = if task.diff.changed_files.is_empty() {
            "nenhum".to_string()
        } else {
            task.diff.changed_files.join(", ")
        };
        out.push_str(&format!(
            "| {} — {} | {} | {} |\n",
            escape_cell(&task.task.id),
            escape_cell(&task.task.title),
            task.status.label(),
            escape_cell(&files)
        ));
    }
    out.push('\n');

    out.push_str("## Resumo da implementação\n\n");
    for task in tasks {
        out.push_str(&format!("### {} — {}\n\n", task.task.id, task.task.title));
        match task.status {
            TaskRunStatus::Succeeded => out.push_str("- Status: concluída.\n"),
            TaskRunStatus::Failed => out.push_str("- Status: falhou durante a execução.\n"),
            TaskRunStatus::Skipped => {
                out.push_str("- Status: pulada ao aceitar resultado parcial.\n")
            }
            TaskRunStatus::Pending | TaskRunStatus::Running => {
                out.push_str("- Status: não finalizada.\n")
            }
        }
        if let Some(result) = &task.result {
            out.push_str(&format!(
                "- Exit code: `{}` · duração: {}ms.\n",
                result.exit_code, result.duration_ms
            ));
        }
        if !task.report.trim().is_empty() {
            out.push('\n');
            out.push_str(clip_chars(task.report.trim(), 4_000).trim());
            out.push_str("\n\n");
        } else if !task.trace.is_empty() {
            out.push_str("\nTrace final:\n");
            for item in task
                .trace
                .iter()
                .rev()
                .take(5)
                .collect::<Vec<_>>()
                .into_iter()
                .rev()
            {
                out.push_str(&format!("- {item}\n"));
            }
            out.push('\n');
        }
    }

    out.push_str("## Arquivos alterados\n\n");
    let files = tasks
        .iter()
        .flat_map(|task| task.diff.changed_files.iter().cloned())
        .collect::<BTreeSet<_>>();
    if files.is_empty() {
        out.push_str("- Nenhum arquivo alterado foi detectado pelo snapshot da TUI.\n\n");
    } else {
        for file in files {
            out.push_str(&format!("- `{file}`\n"));
        }
        out.push('\n');
    }
    if let Some(git_summary) = git_summary.filter(|value| !value.trim().is_empty()) {
        out.push_str("### Resumo Git\n\n```text\n");
        out.push_str(git_summary.trim());
        out.push_str("\n```\n\n");
    }

    out.push_str("## Testes e evidências\n\n");
    for task in tasks {
        out.push_str(&format!("- `{}`: {}.\n", task.task.id, task.status.label()));
    }
    out.push('\n');

    out.push_str("## Riscos e pendências\n\n");
    let failed = tasks
        .iter()
        .filter(|task| task.status == TaskRunStatus::Failed)
        .collect::<Vec<_>>();
    if failed.is_empty() && !partial {
        out.push_str("- Nenhuma falha automática registrada na execução das tasks.\n\n");
    } else {
        if partial {
            out.push_str("- Resultado parcial aceito no TUI; revisar tasks não concluídas antes de aprovar para produção.\n");
        }
        for task in failed {
            out.push_str(&format!(
                "- `{}` — {} falhou; revisar relatório e diff antes de prosseguir.\n",
                task.task.id, task.task.title
            ));
        }
        out.push('\n');
    }

    out.push_str("## Diff por task\n\n");
    for task in tasks {
        out.push_str(&format!("### {} — {}\n\n", task.task.id, task.task.title));
        if task.diff.is_empty() {
            out.push_str("- Sem diff detectado para esta task.\n\n");
            continue;
        }
        out.push_str("Resumo:\n\n```text\n");
        out.push_str(task.diff.summary.trim());
        out.push_str("\n```\n\n");
        out.push_str("Patch:\n\n```diff\n");
        out.push_str(clip_chars(&task.diff.patch, MAX_PATCH_CHARS_PER_TASK).trim());
        out.push_str("\n```\n\n");
    }

    ensure_required_sections(&mut out);
    out
}

fn ensure_required_sections(out: &mut String) {
    for section in [
        "Rastreabilidade",
        "Tarefa",
        "Resumo da implementação",
        "Arquivos alterados",
        "Testes e evidências",
        "Riscos e pendências",
    ] {
        if !out.contains(&format!("## {section}\n")) {
            out.push_str(&format!("## {section}\n\n"));
            out.push_str("- Seção preservada para compatibilidade com o contrato do artefato.\n\n");
        }
    }
}

fn escape_cell(input: &str) -> String {
    input.replace('|', "\\|").replace('\n', " ")
}

fn clip_chars(input: &str, max: usize) -> String {
    if input.chars().count() <= max {
        return input.to_string();
    }
    let mut out = input.chars().take(max).collect::<String>();
    out.push_str("\n[conteúdo truncado]");
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn parses_section_style_tasks() {
        let md = "\
## Backlog

### TSK-01 — Validar contrato real

- Objetivo: mapear arquivos

### TSK-02 — Implementar ordenação

- Dependências: TSK-01
- Objetivo: alterar listagem
";
        let tasks = parse_execution_tasks(md);
        assert_eq!(tasks.len(), 2);
        assert_eq!(tasks[0].id, "TSK-01");
        assert_eq!(tasks[0].title, "Validar contrato real");
        assert!(tasks[0].independent);
        assert_eq!(tasks[1].dependencies, vec!["TSK-01"]);
        assert!(!tasks[1].independent);
    }

    #[test]
    fn parses_table_style_tasks() {
        let md = "\
## Backlog

| ID | Título | Estimativa | Dependências |
|----|--------|------------|--------------|
| T-01 | Base | P | — |
| T-02 | UI | P | T-01 |

### Tasks independentes entre si

- T-02 pode rodar em paralelo.
";
        let tasks = parse_execution_tasks(md);
        assert_eq!(tasks.len(), 2);
        assert_eq!(tasks[0].id, "T-01");
        assert!(tasks[0].independent);
        assert!(tasks[1].independent);
    }

    #[test]
    fn merges_table_tasks_with_prompt_sections() {
        let md = "\
## Backlog

| ID | Título | Estimativa | Dependências | Arquivos |
|----|--------|------------|--------------|----------|
| T-01 | Base | P | — | src/base.rs |
| T-02 | UI | P | T-01 | src/ui.rs |

## Prompts Agent

### T-02 — UI detalhada

- Dependências: T-01
- Objetivo: implementar a interface principal.

#### Prompt Agent
Execute apenas a UI, rode testes focados e reporte evidências.
";
        let tasks = parse_execution_tasks(md);
        assert_eq!(tasks.len(), 2);
        assert_eq!(tasks[1].id, "T-02");
        assert_eq!(tasks[1].title, "UI");
        assert_eq!(tasks[1].dependencies, vec!["T-01"]);
        assert!(tasks[1].body.contains("Prompt Agent"));
        assert!(tasks[1].body.contains("Execute apenas a UI"));
    }

    #[test]
    fn snapshot_diff_detects_added_modified_removed_files_without_git() {
        let dir = tempfile::tempdir().unwrap();
        fs::create_dir_all(dir.path().join("src")).unwrap();
        fs::write(dir.path().join("src/a.txt"), "one\n").unwrap();
        fs::write(dir.path().join("src/remove.txt"), "bye\n").unwrap();
        let before = capture_workspace_snapshot(dir.path(), None).unwrap();

        fs::write(dir.path().join("src/a.txt"), "one\ntwo\n").unwrap();
        fs::write(dir.path().join("src/new.txt"), "new\n").unwrap();
        fs::remove_file(dir.path().join("src/remove.txt")).unwrap();
        fs::create_dir_all(dir.path().join("node_modules/pkg")).unwrap();
        fs::write(dir.path().join("node_modules/pkg/ignored.txt"), "ignored").unwrap();
        let after = capture_workspace_snapshot(dir.path(), None).unwrap();

        let diff = diff_snapshots(&before, &after);
        assert!(diff.summary.contains("M src/a.txt"));
        assert!(diff.summary.contains("A src/new.txt"));
        assert!(diff.summary.contains("D src/remove.txt"));
        assert!(!diff.summary.contains("node_modules"));
        assert!(diff.patch.contains("--- a/src/a.txt"));
    }

    #[test]
    fn snapshot_excludes_artifact_dir() {
        let dir = tempfile::tempdir().unwrap();
        let artifact = dir.path().join("docs/feat");
        fs::create_dir_all(&artifact).unwrap();
        fs::write(artifact.join("06-execution.md"), "ignored").unwrap();

        let snapshot = capture_workspace_snapshot(dir.path(), Some(&artifact)).unwrap();

        assert!(snapshot.files.is_empty());
    }

    #[test]
    fn compose_execution_artifact_contains_required_sections_and_diff() {
        let task = ExecutionTask {
            id: "TSK-01".to_string(),
            title: "Alterar lista".to_string(),
            body: String::new(),
            dependencies: Vec::new(),
            independent: true,
        };
        let artifact = compose_execution_artifact(
            "Minha Feature",
            &[CompletedTask {
                task,
                status: TaskRunStatus::Succeeded,
                report: "Implementado.".to_string(),
                diff: DiffReport {
                    changed_files: vec!["src/a.rs".to_string()],
                    summary: "M src/a.rs".to_string(),
                    patch: "--- a/src/a.rs\n+++ b/src/a.rs\n".to_string(),
                },
                result: None,
                trace: Vec::new(),
            }],
            Some("git status --short:\n M src/a.rs"),
            false,
        );
        assert!(artifact.contains("## Rastreabilidade"));
        assert!(artifact.contains("## Diff por task"));
        assert!(artifact.contains("src/a.rs"));
    }
}
