use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Output, Stdio};

use serde_json::Value;
use serde_yaml::Value as YamlValue;
use tempfile::tempdir;

fn sdd_bin() -> &'static str {
    env!("CARGO_BIN_EXE_sdd")
}

fn repo_root() -> &'static Path {
    Path::new(env!("CARGO_MANIFEST_DIR"))
}

fn collect_relative_files(base: &Path) -> Vec<String> {
    fn visit(base: &Path, current: &Path, out: &mut Vec<String>) {
        let mut entries = fs::read_dir(current)
            .unwrap_or_else(|err| panic!("read dir {}: {err}", current.display()))
            .map(|entry| entry.expect("read dir entry").path())
            .collect::<Vec<_>>();
        entries.sort();
        for path in entries {
            if path.is_dir() {
                visit(base, &path, out);
            } else if path.is_file() {
                out.push(
                    path.strip_prefix(base)
                        .expect("relative path")
                        .to_string_lossy()
                        .replace('\\', "/"),
                );
            }
        }
    }

    let mut files = Vec::new();
    visit(base, base, &mut files);
    files
}

fn assert_directory_mirror(source: &Path, mirror: &Path) {
    let source_files = collect_relative_files(source);
    let mirror_files = collect_relative_files(mirror);
    assert_eq!(
        source_files,
        mirror_files,
        "{} drifted from {}",
        mirror.display(),
        source.display()
    );
    for rel in source_files {
        let source_text =
            fs::read_to_string(source.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
        let mirror_text =
            fs::read_to_string(mirror.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
        assert_eq!(
            source_text,
            mirror_text,
            "{} drifted from {}",
            mirror.join(&rel).display(),
            source.join(&rel).display()
        );
    }
}

fn run(cwd: &Path, args: &[&str]) -> Output {
    run_with(cwd, args, None, &[])
}

fn run_with(cwd: &Path, args: &[&str], input: Option<&str>, envs: &[(&str, &Path)]) -> Output {
    let mut command = Command::new(sdd_bin());
    command
        .args(args)
        .current_dir(cwd)
        .env_remove("CLAUDE_PROJECT_DIR")
        .env_remove("SDD_LAYER_ROOT")
        .env_remove("SDD_PROJECT_ROOT")
        .env_remove("SDD_USAGE_INPUT_TOKENS")
        .env_remove("SDD_USAGE_CACHED_INPUT_TOKENS")
        .env_remove("SDD_USAGE_OUTPUT_TOKENS")
        .env_remove("SDD_USAGE_REASONING_OUTPUT_TOKENS")
        .env_remove("SDD_USAGE_TOTAL_TOKENS")
        .env_remove("SDD_GENERATION_STARTED_AT")
        .env_remove("SDD_GENERATION_EXECUTION_STARTED_AT")
        .env_remove("SDD_GENERATION_FINISHED_AT")
        .env_remove("SDD_GENERATION_DURATION_MS")
        .env_remove("SDD_GENERATION_REASONING_DURATION_MS")
        .env_remove("SDD_GENERATION_EXECUTION_DURATION_MS")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    for (key, value) in envs {
        command.env(key, value);
    }
    if input.is_some() {
        command.stdin(Stdio::piped());
    } else {
        command.stdin(Stdio::null());
    }
    let mut child = command.spawn().expect("spawn sdd");
    if let Some(input) = input {
        child
            .stdin
            .as_mut()
            .expect("stdin")
            .write_all(input.as_bytes())
            .expect("write stdin");
    }
    child.wait_with_output().expect("wait sdd")
}

fn run_with_env_values(
    cwd: &Path,
    args: &[&str],
    input: Option<&str>,
    envs: &[(&str, &str)],
) -> Output {
    let mut command = Command::new(sdd_bin());
    command
        .args(args)
        .current_dir(cwd)
        .env_remove("CLAUDE_PROJECT_DIR")
        .env_remove("SDD_LAYER_ROOT")
        .env_remove("SDD_PROJECT_ROOT")
        .env_remove("SDD_USAGE_INPUT_TOKENS")
        .env_remove("SDD_USAGE_CACHED_INPUT_TOKENS")
        .env_remove("SDD_USAGE_OUTPUT_TOKENS")
        .env_remove("SDD_USAGE_REASONING_OUTPUT_TOKENS")
        .env_remove("SDD_USAGE_TOTAL_TOKENS")
        .env_remove("SDD_GENERATION_STARTED_AT")
        .env_remove("SDD_GENERATION_EXECUTION_STARTED_AT")
        .env_remove("SDD_GENERATION_FINISHED_AT")
        .env_remove("SDD_GENERATION_DURATION_MS")
        .env_remove("SDD_GENERATION_REASONING_DURATION_MS")
        .env_remove("SDD_GENERATION_EXECUTION_DURATION_MS")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    for (key, value) in envs {
        command.env(key, value);
    }
    if input.is_some() {
        command.stdin(Stdio::piped());
    } else {
        command.stdin(Stdio::null());
    }
    let mut child = command.spawn().expect("spawn sdd");
    if let Some(input) = input {
        child
            .stdin
            .as_mut()
            .expect("stdin")
            .write_all(input.as_bytes())
            .expect("write stdin");
    }
    child.wait_with_output().expect("wait sdd")
}

fn combined(output: &Output) -> String {
    format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

fn assert_portable_orchestration_skill_contract(content: &str) {
    assert!(content.contains(
        "6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff."
    ));
    assert!(content.contains(
        "6a. ADR: create or update Architecture Decision Records from execution evidence."
    ));
    assert!(content.contains(
        "Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`"
    ));
    assert!(content.contains("If `sdd` is not in `PATH`"));
    assert!(content.contains("cargo run --bin sdd -- ..."));
    assert!(content.contains("During `/execution`, use `execution-discipline`"));
    assert!(!content.contains("6. Execution: one task at a time.\n6a. ADR"));
}

const CORE_STAGE_SKILLS: &[&str] = &[
    "idea",
    "prd",
    "techspec",
    "tasks",
    "execution",
    "review",
    "memory",
];

fn assert_stage_skill_contract(root: &Path, prefix: &str, stage: &str) {
    let rel = format!("{prefix}/{stage}/SKILL.md");
    let content = fs::read_to_string(root.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
    assert!(content.starts_with("---\n"), "{rel}");
    assert!(content.contains(&format!("name: {stage}\n")), "{rel}");
    assert!(content.contains("description: "), "{rel}");
    assert!(
        content.contains("delegue estado, persistência e validação ao CLI `sdd`"),
        "{rel}"
    );
    assert!(content.contains(&format!("sdd {stage}")), "{rel}");
    assert!(content.contains("sdd init"), "{rel}");
    assert!(content.contains("sdd artifact save"), "{rel}");
    assert!(content.contains("docs/<slug-da-orquestracao>/"), "{rel}");
    assert!(content.contains("traceability-map.yaml"), "{rel}");
    assert!(
        content.chars().any(|ch| ch as u32 > 0x7f),
        "{rel} should preserve PT-BR accents"
    );
}

fn assert_core_stage_skills(root: &Path, prefix: &str) {
    for stage in CORE_STAGE_SKILLS {
        assert_stage_skill_contract(root, prefix, stage);
    }
}

fn assert_stage_command_fallback(root: &Path, prefix: &str, extension: &str, stage: &str) {
    let rel = format!("{prefix}/{stage}.{extension}");
    let content = fs::read_to_string(root.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
    assert!(
        content.contains("Use a skill ou subagent desta etapa se o client a expuser"),
        "{rel}"
    );
    assert!(
        content.contains(&format!("sdd {stage}")),
        "{rel} should delegate to sdd {stage}"
    );
}

fn assert_stage_command_fallbacks(root: &Path) {
    for stage in CORE_STAGE_SKILLS {
        assert_stage_command_fallback(root, ".cursor/commands", "md", stage);
        assert_stage_command_fallback(root, ".devin/workflows", "md", stage);
        assert_stage_command_fallback(root, ".opencode/commands", "md", stage);
        assert_stage_command_fallback(root, ".trae/commands", "md", stage);
    }
}

fn assert_diagram_command_contract(content: &str) {
    for expected in [
        "Fluxo obrigatório",
        "Gate de estilo",
        "Coleta automática",
        "Validação",
        "Fallback provider-neutral",
        "sdd resolve-input",
        "sdd diagram doctor --name \"<nome-da-orquestracao>\"",
        "--file <arquivo.html|svg|excalidraw>",
        "sdd validate-artifact <tipo> <arquivo.md>",
        "PNG é derivado opcional, não formato principal",
    ] {
        assert!(
            content.contains(expected),
            "diagram command should contain `{expected}`"
        );
    }
}

fn canonical_orchestration_skill() -> &'static str {
    include_str!("../templates/client-skills/orchestration.md")
}

fn assert_success(output: Output) {
    assert!(
        output.status.success(),
        "expected success, got {:?}\n{}",
        output.status.code(),
        combined(&output)
    );
}

fn assert_code(output: Output, expected: i32) {
    assert_eq!(
        output.status.code(),
        Some(expected),
        "unexpected exit code\n{}",
        combined(&output)
    );
}

fn yaml_get<'a>(mapping: &'a serde_yaml::Mapping, key: &str) -> Option<&'a YamlValue> {
    mapping.get(YamlValue::String(key.to_string()))
}

fn artifact_stage_files(yaml: &str) -> Vec<(String, String)> {
    let root: YamlValue = serde_yaml::from_str(yaml).expect("valid yaml");
    let root = root.as_mapping().expect("root mapping");
    let artifacts = yaml_get(root, "artifacts")
        .and_then(YamlValue::as_mapping)
        .expect("artifacts mapping");

    artifacts
        .iter()
        .map(|(stage, metadata)| {
            let stage = stage.as_str().expect("artifact stage key").to_string();
            let file = metadata
                .as_mapping()
                .and_then(|metadata| yaml_get(metadata, "file"))
                .and_then(YamlValue::as_str)
                .unwrap_or("")
                .to_string();
            (stage, file)
        })
        .collect()
}

fn contract_stage_files(yaml: &str) -> Vec<(String, String)> {
    let root: YamlValue = serde_yaml::from_str(yaml).expect("valid yaml");
    let root = root.as_mapping().expect("root mapping");
    let stages = yaml_get(root, "stages")
        .and_then(YamlValue::as_sequence)
        .expect("stages sequence");

    stages
        .iter()
        .map(|stage| {
            let stage = stage.as_mapping().expect("stage mapping");
            let key = yaml_get(stage, "key")
                .and_then(YamlValue::as_str)
                .expect("stage key")
                .to_string();
            let file = yaml_get(stage, "filename")
                .and_then(YamlValue::as_str)
                .expect("stage filename")
                .to_string();
            (key, file)
        })
        .collect()
}

fn assert_no_script_runtime(path: &Path) {
    fn visit(path: &Path) {
        for entry in fs::read_dir(path).expect("read dir") {
            let entry = entry.expect("dir entry");
            let path = entry.path();
            let name = path
                .file_name()
                .and_then(|item| item.to_str())
                .unwrap_or("");
            assert_ne!(
                name,
                "__pycache__",
                "cache directory leaked: {}",
                path.display()
            );
            assert_ne!(
                name,
                "scripts",
                "script directory leaked: {}",
                path.display()
            );
            assert_ne!(
                path.extension().and_then(|item| item.to_str()),
                Some("py"),
                "script file leaked: {}",
                path.display()
            );
            assert_ne!(
                path.extension().and_then(|item| item.to_str()),
                Some("pyc"),
                "compiled script cache leaked: {}",
                path.display()
            );
            if path.is_dir() {
                visit(&path);
            }
        }
    }
    visit(path);
}

fn json_contains_text(value: &Value, needle: &str) -> bool {
    match value {
        Value::String(text) => text.contains(needle),
        Value::Array(items) => items.iter().any(|item| json_contains_text(item, needle)),
        Value::Object(map) => map
            .iter()
            .any(|(key, value)| key.contains(needle) || json_contains_text(value, needle)),
        _ => false,
    }
}

fn install_generic_project(root: &Path) {
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
}

fn create_project_intelligence_artifacts(root: &Path) {
    let name = "Project Intelligence Layer";
    assert_success(run(root, &["init", name]));
    for (stage, input) in [
        ("idea", "mapear contexto do projeto"),
        ("prd", "produto precisa de contexto auditavel"),
        ("techspec", "implementar pacote de contexto deterministico"),
    ] {
        assert_success(run(root, &[stage, "--name", name, "--force", input]));
    }
}

fn prepend_path(bin: &Path) -> String {
    let current = env::var_os("PATH").unwrap_or_default();
    env::join_paths(std::iter::once(bin.to_path_buf()).chain(env::split_paths(&current)))
        .expect("join PATH")
        .to_string_lossy()
        .to_string()
}

fn write_fake_codegraph(bin: &Path) {
    fs::create_dir_all(bin).unwrap();
    let executable = if cfg!(windows) {
        bin.join("codegraph.cmd")
    } else {
        bin.join("codegraph")
    };
    let body = if cfg!(windows) {
        "@echo off\r\necho %* > codegraph-args.txt\r\nmkdir .codegraph 2>nul\r\nexit /b 0\r\n"
    } else {
        "#!/bin/sh\nprintf '%s\\n' \"$*\" > codegraph-args.txt\nmkdir -p .codegraph\n"
    };
    fs::write(&executable, body).unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut permissions = fs::metadata(&executable).unwrap().permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&executable, permissions).unwrap();
    }
}

#[test]
fn detects_api_presets_and_installs_them() {
    let tmp = tempdir().unwrap();
    let project = tmp.path();

    fs::write(
        project.join("pyproject.toml"),
        "[project]\nname = \"api\"\n",
    )
    .unwrap();
    let output = run(project, &["init", "--dry-run"]);
    assert_success(output);
    assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: python-api"));
    fs::remove_file(project.join("pyproject.toml")).unwrap();

    fs::write(
        project.join("Cargo.toml"),
        "[package]\nname = \"api\"\nversion = \"0.1.0\"\n[dependencies]\naxum = \"0.7\"\n",
    )
    .unwrap();
    assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: rust-api"));
    fs::remove_file(project.join("Cargo.toml")).unwrap();

    fs::write(
        project.join("package.json"),
        r#"{"dependencies":{"hono":"latest"}}"#,
    )
    .unwrap();
    assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: hono-api"));

    for preset in ["python-api", "rust-api", "hono-api"] {
        let target = tempdir().unwrap();
        assert_success(run(
            repo_root(),
            &[
                "install",
                target.path().to_str().unwrap(),
                "--preset",
                preset,
            ],
        ));
        let config = fs::read_to_string(target.path().join("sdd.config.yaml")).unwrap();
        assert!(config.contains(&format!("name: {preset}-project")));
        assert_success(run(target.path(), &["doctor"]));
        assert_no_script_runtime(target.path());
    }
}

#[test]
fn doctor_output_uses_tui_summary_and_groups_repeated_warnings() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let workflows = root.join(".github/workflows");
    fs::create_dir_all(&workflows).unwrap();
    fs::write(
        workflows.join("ci.yml"),
        "name: ci\non: [push]\njobs:\n  placeholder:\n    runs-on: ubuntu-latest\n    steps: []\n",
    )
    .unwrap();

    let output = run(root, &["doctor"]);
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("╭─ SDD doctor"));
    assert!(text.contains("│ ✓ PASS"));
    assert!(text.contains("failures ·"));
    assert!(text.contains("◇ Failures (0)"));
    assert!(text.contains("  ✓ none"));
    assert!(text.contains("◇ Warnings"));
    assert!(text.contains("⚠ CI detectado, mas workflow não contém check esperado"));
    assert!(text.contains("├─ `cargo test`"));
    assert!(text.contains("├─ `cargo clippy`"));
    assert!(text.contains("└─ `sdd ci`"));
    assert!(!text.contains("- CI detectado"));
}

#[test]
fn install_output_uses_rail_steps_quick_start_and_relative_paths() {
    let target = tempdir().unwrap();
    let output = run(
        repo_root(),
        &[
            "install",
            target.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--dry-run",
        ],
    );
    let text = combined(&output);
    assert_success(output);

    assert!(text.contains("│ SDD install"));
    assert!(text.contains("◇ Project root"));
    assert!(text.contains("◆ Camada SDD sincronizada"));
    assert!(text.contains("Planejadas"));
    assert!(text.contains("copiar ./AGENTS.md"));
    assert!(text.contains("◇ Quick start"));
    assert!(text.contains("sdd doctor"));
    assert!(text.contains("└ Done! SDD install complete."));
    assert!(!text.contains("Timeline"));
    assert!(!text.contains("Ações"));
    assert!(!text.contains(&format!(
        "dry-run copy {}",
        target.path().join("AGENTS.md").display()
    )));
}

#[test]
fn init_auto_indexes_codegraph_when_available() {
    let target = tempdir().unwrap();
    let bin = tempdir().unwrap();
    write_fake_codegraph(bin.path());
    let path = prepend_path(bin.path());

    let output = run_with_env_values(
        repo_root(),
        &[
            "init",
            "--root",
            target.path().to_str().unwrap(),
            "--preset",
            "generic",
        ],
        None,
        &[("PATH", &path)],
    );
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("CodeGraph: ran codegraph init -i ."));
    assert!(target.path().join(".codegraph").is_dir());
    assert!(fs::read_to_string(target.path().join(".gitignore"))
        .unwrap()
        .contains(".codegraph/"));
    assert_eq!(
        fs::read_to_string(target.path().join("codegraph-args.txt"))
            .unwrap()
            .trim(),
        "init -i ."
    );
}

#[test]
fn init_dry_run_reports_codegraph_auto_index_without_running() {
    let target = tempdir().unwrap();
    let bin = tempdir().unwrap();
    write_fake_codegraph(bin.path());
    let path = prepend_path(bin.path());

    let output = run_with_env_values(
        repo_root(),
        &[
            "init",
            "--root",
            target.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--dry-run",
        ],
        None,
        &[("PATH", &path)],
    );
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("CodeGraph: dry-run codegraph init -i ."));
    assert!(!target.path().join(".codegraph").exists());
    assert!(!target.path().join("codegraph-args.txt").exists());
}

#[test]
fn install_layouts_ci_and_cleanup_are_rust_only() {
    let compact = tempdir().unwrap();
    let output = run(
        repo_root(),
        &[
            "install",
            compact.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--with-ci",
        ],
    );
    assert_success(output);
    assert!(compact.path().join(".sdd/tests").is_dir());
    assert!(compact.path().join(".github/workflows/sdd.yml").is_file());
    assert!(compact.path().join(".devin/config.json").is_file());
    assert!(compact.path().join(".devin/rules/sdd.md").is_file());
    assert!(compact
        .path()
        .join(".devin/agents/sdd-orchestrator/AGENT.md")
        .is_file());
    assert!(compact
        .path()
        .join(".devin/skills/orchestration/SKILL.md")
        .is_file());
    assert!(compact.path().join(".devin/skills/prd/SKILL.md").is_file());
    assert!(compact.path().join(".devin/workflows/sdd.md").is_file());
    assert!(compact
        .path()
        .join(".devin/workflows/orchestration.md")
        .is_file());
    assert!(compact.path().join(".devin/workflows/prd.md").is_file());
    assert!(compact.path().join(".cursor/model-routing.yaml").is_file());
    assert!(compact.path().join(".cursor/rules/sdd.mdc").is_file());
    assert!(compact.path().join(".cursor/commands/sdd.md").is_file());
    assert!(compact
        .path()
        .join(".cursor/commands/orchestration.md")
        .is_file());
    assert!(compact
        .path()
        .join(".cursor/agents/sdd-orchestrator.md")
        .is_file());
    assert!(compact
        .path()
        .join(".cursor/skills/orchestration/SKILL.md")
        .is_file());
    assert!(compact
        .path()
        .join(".opencode/commands/orchestration.md")
        .is_file());
    assert!(compact
        .path()
        .join(".agents/skills/orchestration/SKILL.md")
        .is_file());
    assert!(compact
        .path()
        .join(".trae/commands/orchestration.md")
        .is_file());
    assert!(compact
        .path()
        .join(".trae/rules/00-sdd-principles.md")
        .is_file());
    assert!(compact
        .path()
        .join(".trae/skills/orchestration/SKILL.md")
        .is_file());
    assert!(compact.path().join(".sdd/clients/README.md").is_file());
    assert!(!compact.path().join("presets").exists());
    assert!(!compact.path().join("scripts").exists());
    assert_success(run(compact.path(), &["doctor"]));
    assert_success(run(compact.path(), &["clients", "doctor"]));
    assert_no_script_runtime(compact.path());

    let initialized = tempdir().unwrap();
    assert_success(run(initialized.path(), &["init", "--preset", "generic"]));
    assert!(initialized.path().join(".devin/config.json").is_file());
    assert!(initialized.path().join(".devin/rules/sdd.md").is_file());
    assert!(initialized
        .path()
        .join(".devin/agents/sdd-orchestrator/AGENT.md")
        .is_file());
    assert!(initialized
        .path()
        .join(".devin/skills/orchestration/SKILL.md")
        .is_file());
    assert!(initialized
        .path()
        .join(".devin/skills/prd/SKILL.md")
        .is_file());
    assert!(initialized.path().join(".devin/workflows/sdd.md").is_file());
    assert!(initialized
        .path()
        .join(".devin/workflows/orchestration.md")
        .is_file());
    assert!(initialized.path().join(".devin/workflows/prd.md").is_file());
    assert!(initialized
        .path()
        .join(".cursor/model-routing.yaml")
        .is_file());
    assert!(initialized.path().join(".cursor/rules/sdd.mdc").is_file());
    assert!(initialized.path().join(".cursor/commands/sdd.md").is_file());
    assert!(initialized
        .path()
        .join(".cursor/commands/orchestration.md")
        .is_file());
    assert!(initialized
        .path()
        .join(".cursor/agents/sdd-orchestrator.md")
        .is_file());
    assert!(initialized
        .path()
        .join(".cursor/skills/orchestration/SKILL.md")
        .is_file());
    assert!(initialized
        .path()
        .join(".trae/commands/orchestration.md")
        .is_file());
    assert_success(run(initialized.path(), &["clients", "doctor"]));

    let flat = tempdir().unwrap();
    assert_success(run(
        repo_root(),
        &[
            "install",
            flat.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--layout",
            "flat",
        ],
    ));
    assert!(flat.path().join("presets").is_dir());
    assert!(!flat.path().join(".sdd").exists());
    assert!(!flat.path().join("scripts").exists());

    let preserved = flat.path().join("docs/minha-orquestracao");
    fs::create_dir_all(&preserved).unwrap();
    fs::write(
        preserved.join("traceability-map.yaml"),
        "orchestration: {}\n",
    )
    .unwrap();
    assert_success(run(
        repo_root(),
        &[
            "install",
            flat.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--force",
            "--cleanup-legacy",
        ],
    ));
    assert!(flat.path().join(".sdd/presets").is_dir());
    assert!(preserved.exists());
    for rel in [
        "presets",
        "schemas",
        "templates",
        "adapters",
        "clients",
        "tests",
        "examples",
        "scripts",
    ] {
        assert!(
            !flat.path().join(rel).exists(),
            "legacy path still exists: {rel}"
        );
    }
}

#[test]
fn install_and_update_skip_bot_bundle_when_install_is_declined() {
    let target = tempdir().unwrap();
    let root = target.path();

    assert_success(run_with(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
        Some("n\n"),
        &[],
    ));
    assert!(!root.join("bot").exists());
    assert!(!root.join(".sdd/bot").exists());

    assert_success(run_with(root, &["update"], Some("n\n"), &[]));
    assert!(!root.join("bot").exists());
    assert!(!root.join(".sdd/bot").exists());

    let flat = tempdir().unwrap();
    assert_success(run_with(
        repo_root(),
        &[
            "install",
            flat.path().to_str().unwrap(),
            "--preset",
            "generic",
            "--layout",
            "flat",
        ],
        Some("n\n"),
        &[],
    ));
    assert!(!flat.path().join("bot").exists());
}

#[test]
fn update_refreshes_installed_package_without_losing_project_state() {
    let target = tempdir().unwrap();
    let root = target.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
    let installed_mcp =
        serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
            .unwrap();
    // O `.mcp.json` embute o root canonicalizado com separador `/` normalizado
    // (ver ensure_existing_dir + render_mcp_json). No Windows, tempdir() pode
    // devolver um path não-canônico (8.3 short names); canonicalizamos e
    // removemos o prefixo verbatim `\\?\` para comparar corretamente.
    let root_canonical = {
        let c = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
        let s = c.to_string_lossy().into_owned();
        let stripped = s.strip_prefix(r"\\?\").unwrap_or(&s).to_string();
        stripped.replace('\\', "/")
    };
    assert!(json_contains_text(&installed_mcp, &root_canonical));
    assert!(root.join(".cursor/mcp.json").is_file());

    let config_path = root.join("sdd.config.yaml");
    // Normaliza CRLF→LF: em clones Windows com core.autocrlf o preset vem com \r\n,
    // e os `replace`/asserções abaixo assumem LF.
    let mut config = fs::read_to_string(&config_path)
        .unwrap()
        .replace("\r\n", "\n");
    config.push_str("\ncustom_marker: keep-me\n");
    config = config.replace(
        "\nruntime:\n",
        "\n    custom:\n      kind: openai-compatible\n      auth_env: CUSTOM_PROVIDER_KEY\n      model: custom-model\n      capabilities: [chat]\n      enabled: true\n\nruntime:\n",
    );
    fs::write(&config_path, config).unwrap();

    let artifact_dir = root.join("docs/minha-orquestracao");
    fs::create_dir_all(&artifact_dir).unwrap();
    fs::write(
        artifact_dir.join("traceability-map.yaml"),
        "orchestration: {}\n",
    )
    .unwrap();

    let custom_skill = root.join(".agents/skills/custom-stack/SKILL.md");
    fs::create_dir_all(custom_skill.parent().unwrap()).unwrap();
    fs::write(&custom_skill, "# Custom Stack\n").unwrap();

    fs::remove_dir_all(root.join(".cursor")).unwrap();
    fs::remove_dir_all(root.join(".devin")).unwrap();
    fs::remove_dir_all(root.join(".opencode")).unwrap();
    fs::remove_dir_all(root.join(".trae")).unwrap();
    fs::remove_dir_all(root.join(".agents/skills/orchestration")).unwrap();
    fs::remove_dir_all(root.join(".sdd/clients")).unwrap();
    fs::remove_file(root.join(".mcp.json")).unwrap();

    assert_success(run(root, &["update"]));
    assert!(root.join("AGENTS.md").is_file());
    let updated_mcp =
        serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
            .unwrap();
    assert!(json_contains_text(&updated_mcp, &root_canonical));
    assert!(root.join(".devin/config.json").is_file());
    assert!(root.join(".devin/rules/sdd.md").is_file());
    assert!(root
        .join(".devin/agents/sdd-orchestrator/AGENT.md")
        .is_file());
    assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".devin/workflows/orchestration.md").is_file());
    assert!(root.join(".devin/workflows/prd.md").is_file());
    assert!(root.join(".cursor/model-routing.yaml").is_file());
    assert!(root.join(".cursor/mcp.json").is_file());
    assert!(root.join(".cursor/rules/sdd.mdc").is_file());
    assert!(root.join(".cursor/rules/codegraph.mdc").is_file());
    assert!(root.join(".cursor/commands/sdd.md").is_file());
    assert!(root.join(".cursor/commands/orchestration.md").is_file());
    assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
    assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".opencode/commands/orchestration.md").is_file());
    assert!(root.join(".trae/commands/orchestration.md").is_file());
    assert!(root.join(".agents/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".sdd/clients/README.md").is_file());

    fs::remove_dir_all(root.join(".cursor")).unwrap();
    fs::remove_dir_all(root.join(".devin")).unwrap();
    fs::remove_dir_all(root.join(".opencode")).unwrap();
    fs::remove_dir_all(root.join(".trae")).unwrap();
    fs::remove_dir_all(root.join(".agents/skills/orchestration")).unwrap();
    fs::remove_dir_all(root.join(".sdd/clients")).unwrap();
    fs::remove_file(root.join(".mcp.json")).unwrap();

    assert_success(run(
        repo_root(),
        &["update", "--root", root.to_str().unwrap()],
    ));
    let preserved_config = fs::read_to_string(&config_path).unwrap();
    assert!(preserved_config.contains("custom_marker: keep-me"));
    assert!(preserved_config.contains("CUSTOM_PROVIDER_KEY"));
    assert!(preserved_config.contains("custom-model"));
    assert!(artifact_dir.join("traceability-map.yaml").is_file());
    assert!(custom_skill.is_file());
    let repo_update_mcp =
        serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
            .unwrap();
    assert!(json_contains_text(&repo_update_mcp, &root_canonical));
    assert!(root.join(".devin/config.json").is_file());
    assert!(root.join(".devin/rules/sdd.md").is_file());
    assert!(root
        .join(".devin/agents/sdd-orchestrator/AGENT.md")
        .is_file());
    assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".devin/workflows/orchestration.md").is_file());
    assert!(root.join(".devin/workflows/prd.md").is_file());
    assert!(root.join(".cursor/model-routing.yaml").is_file());
    assert!(root.join(".cursor/mcp.json").is_file());
    assert!(root.join(".cursor/rules/sdd.mdc").is_file());
    assert!(root.join(".cursor/rules/codegraph.mdc").is_file());
    assert!(root.join(".cursor/commands/sdd.md").is_file());
    assert!(root.join(".cursor/commands/orchestration.md").is_file());
    assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
    assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".opencode/commands/orchestration.md").is_file());
    assert!(root.join(".trae/commands/orchestration.md").is_file());
    assert!(root.join(".agents/skills/orchestration/SKILL.md").is_file());
    assert!(root.join(".sdd/clients/README.md").is_file());
    assert_success(run(root, &["clients", "doctor"]));

    assert_success(run(
        repo_root(),
        &[
            "update",
            "--root",
            root.to_str().unwrap(),
            "--preset",
            "backend-node",
            "--update-config",
        ],
    ));
    let refreshed_config = fs::read_to_string(&config_path).unwrap();
    assert!(refreshed_config.contains("name: backend-node-project"));
    assert!(!refreshed_config.contains("custom_marker: keep-me"));
    assert_success(run(
        repo_root(),
        &["upgrade", "--root", root.to_str().unwrap(), "--dry-run"],
    ));
}

#[test]
fn install_and_update_merge_existing_agent_docs() {
    let target = tempdir().unwrap();
    let root = target.path();
    fs::write(
        root.join("AGENTS.md"),
        "# Projeto\n\nRegras locais do Codex.\n",
    )
    .unwrap();
    fs::write(
        root.join("CLAUDE.md"),
        "# Projeto Claude\n\nRegras locais do Claude.\n",
    )
    .unwrap();

    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    let agents = fs::read_to_string(root.join("AGENTS.md")).unwrap();
    assert!(agents.contains("# Projeto"));
    assert!(agents.contains("Regras locais do Codex."));
    assert!(agents.contains("<!-- sdd-layer:start -->"));
    assert!(agents.contains("# AGENTS.md — Camada de orquestração SDLC (Spec-Driven)"));
    assert_eq!(agents.matches("<!-- sdd-layer:start -->").count(), 1);

    let claude = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
    assert!(claude.contains("# Projeto Claude"));
    assert!(claude.contains("Regras locais do Claude."));
    assert!(claude.contains("<!-- sdd-layer:start -->"));
    assert!(claude.contains("# CLAUDE.md — Camada de orquestração SDLC (Spec-Driven)"));
    assert_eq!(claude.matches("<!-- sdd-layer:start -->").count(), 1);

    fs::write(
        root.join("AGENTS.md"),
        format!("{agents}\n## Regras após SDD\n\nContinue preservando esta seção.\n"),
    )
    .unwrap();

    assert_success(run(root, &["update"]));

    let agents_after_update = fs::read_to_string(root.join("AGENTS.md")).unwrap();
    assert!(agents_after_update.contains("# Projeto"));
    assert!(agents_after_update.contains("Regras locais do Codex."));
    assert!(agents_after_update.contains("## Regras após SDD"));
    assert!(agents_after_update.contains(
        "<!-- sdd-layer:end -->\n\n## Regras após SDD\n\nContinue preservando esta seção."
    ));
    assert_eq!(
        agents_after_update
            .matches("<!-- sdd-layer:start -->")
            .count(),
        1
    );

    let claude_after_update = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
    assert!(claude_after_update.contains("# Projeto Claude"));
    assert!(claude_after_update.contains("Regras locais do Claude."));
    assert_eq!(
        claude_after_update
            .matches("<!-- sdd-layer:start -->")
            .count(),
        1
    );
}

#[test]
fn update_ignores_parent_project_installation_as_package_source() {
    let target = tempdir().unwrap();
    let parent = target.path();
    let root = parent.join("apps/child");
    fs::create_dir_all(&root).unwrap();
    fs::write(parent.join("AGENTS.md"), "# Parent SDD\n").unwrap();
    fs::write(parent.join("CLAUDE.md"), "# Parent SDD\n").unwrap();
    for rel in [".agents", ".claude", ".codex", ".cursor"] {
        fs::create_dir_all(parent.join(rel)).unwrap();
    }
    fs::create_dir_all(parent.join(".sdd/presets")).unwrap();

    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
    fs::remove_dir_all(root.join(".devin")).unwrap();

    assert_success(run(&root, &["update"]));
    assert!(root.join(".devin/config.json").is_file());
    assert!(root.join(".devin/rules/sdd.md").is_file());
}

#[test]
fn demand_enqueue_tick_and_human_approval_flow() {
    let t = tempdir().unwrap();
    let root = t.path();
    fs::write(root.join("sdd.config.yaml"), "approvers:\n  cli: [alan]\n").unwrap();
    let r = root.to_str().unwrap();

    // Enfileira a demanda.
    let out = run(
        root,
        &[
            "demand",
            "--root",
            r,
            "enqueue",
            "--id",
            "DEM-1",
            "--type",
            "story",
            "--title",
            "Teste Esteira",
        ],
    );
    assert!(combined(&out).contains("enfileirada"), "{}", combined(&out));
    assert_success(out);

    // Tick 1 produz a Idea; tick 2 produz o PRD e pausa no gate.
    assert_success(run(root, &["auto", "--root", r, "tick"]));
    let out = run(root, &["auto", "--root", r, "tick"]);
    let txt = combined(&out);
    assert!(txt.contains("paused") && txt.contains("prd"), "{txt}");
    assert_success(out);

    // Status mostra aguardando aprovação no PRD.
    let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
    assert!(
        combined(&out).contains("awaiting_approval"),
        "{}",
        combined(&out)
    );
    assert_success(out);

    // Artefatos gerados em disco.
    assert!(root.join("docs/teste-esteira/01-idea.md").is_file());
    assert!(root.join("docs/teste-esteira/02-prd.md").is_file());

    // Aprovador não autorizado é negado (default-deny).
    let out = run(
        root,
        &[
            "demand", "--root", r, "approve", "DEM-1", "--gate", "prd", "--by", "mallory",
        ],
    );
    assert!(
        combined(&out).contains("não autorizado"),
        "{}",
        combined(&out)
    );
    assert_code(out, 1);

    // Aprovador autorizado avança o fluxo.
    let out = run(
        root,
        &[
            "demand", "--root", r, "approve", "DEM-1", "--gate", "prd", "--by", "alan",
        ],
    );
    assert!(combined(&out).contains("aprovado"), "{}", combined(&out));
    assert_success(out);

    // Cursor avançou para a Tech Spec.
    let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
    assert!(combined(&out).contains("techspec"), "{}", combined(&out));
    assert_success(out);
}

#[test]
fn auto_run_drives_to_gate_with_audit() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    assert_success(run(
        root,
        &[
            "demand",
            "--root",
            r,
            "enqueue",
            "--id",
            "DEM-1",
            "--type",
            "story",
            "--title",
            "Esteira Run",
        ],
    ));

    // auto run leva a demanda até o gate do PRD e para, com notify do gate.
    let run_out = run(root, &["auto", "--root", r, "run"]);
    let run_txt = combined(&run_out);
    assert!(run_txt.contains("aguardando aprovação"), "{run_txt}");
    assert!(run_txt.contains("sdd demand approve"), "{run_txt}");
    assert_success(run_out);

    let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
    assert!(
        combined(&out).contains("awaiting_approval"),
        "{}",
        combined(&out)
    );
    assert_success(out);

    // Auditoria append-only registrada em .sdd/orchestrator.jsonl, com pedido de gate.
    let audit = fs::read_to_string(root.join(".sdd/orchestrator.jsonl")).unwrap();
    assert!(audit.contains("\"event\":\"tick\""), "{audit}");
    assert!(audit.contains("\"event\":\"gate_request\""), "{audit}");
}

#[test]
fn auto_tick_real_uses_headless_runner() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    // Provider sem adapter direto → runner real cai no harness determinístico (scaffold).
    fs::write(
        root.join("sdd.config.yaml"),
        "providers:\n  default: custom\n  offline: true\napprovers:\n  cli: [alan]\n",
    )
    .unwrap();
    assert_success(run(
        root,
        &[
            "demand",
            "--root",
            r,
            "enqueue",
            "--id",
            "DEM-1",
            "--type",
            "story",
            "--title",
            "Busca Avancada",
        ],
    ));

    let out = run(root, &["auto", "--root", r, "tick", "--real"]);
    assert!(
        combined(&out).contains("busca-avancada"),
        "{}",
        combined(&out)
    );
    assert_success(out);

    // O HeadlessRunner produz um artefato real (com seções), não o stub do FakeRunner.
    let idea = fs::read_to_string(root.join("docs/busca-avancada/01-idea.md")).unwrap();
    assert!(idea.contains("## Rastreabilidade"), "{idea}");
    assert!(!idea.contains("FakeRunner"), "{idea}");
}

#[test]
fn eval_stage_and_quality_report_persist_evidence() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
    assert_success(run(root, &["artifact", "--root", r, "init", "Eval OK"]));
    assert_success(run(
        root,
        &[
            "artifact",
            "--root",
            r,
            "save",
            "Eval OK",
            "prd",
            "--file",
            fixture.to_str().unwrap(),
            "--state",
            "recorded",
        ],
    ));

    let out = run(
        root,
        &[
            "eval", "--root", r, "stage", "--name", "Eval OK", "--stage", "prd", "--json",
        ],
    );
    assert_success(out);
    let event_log = fs::read_to_string(root.join(".sdd/evaluations.jsonl")).unwrap();
    assert!(event_log.contains("eval_stage"), "{event_log}");

    let quality = run(
        root,
        &[
            "quality", "--root", r, "report", "--name", "Eval OK", "--json",
        ],
    );
    let text = combined(&quality);
    assert_success(quality);
    let parsed: Value = serde_json::from_str(&text).unwrap();
    assert_eq!(parsed["status"], "pass");
    assert_eq!(parsed["evaluation"]["critical_count"], 0);
    let event_log = fs::read_to_string(root.join(".sdd/evaluations.jsonl")).unwrap();
    assert!(event_log.contains("quality_report"), "{event_log}");
}

#[test]
fn eval_stage_accepts_visual_companion_diagram() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    let fixture = repo_root().join("tests/fixtures/good/prd-visual-companion.md");
    assert_success(run(root, &["artifact", "--root", r, "init", "Eval Visual"]));
    assert_success(run(
        root,
        &[
            "artifact",
            "--root",
            r,
            "save",
            "Eval Visual",
            "prd",
            "--file",
            fixture.to_str().unwrap(),
            "--state",
            "recorded",
        ],
    ));

    assert_success(run(
        root,
        &[
            "eval",
            "--root",
            r,
            "stage",
            "--name",
            "Eval Visual",
            "--stage",
            "prd",
            "--json",
        ],
    ));
}

#[test]
fn auto_run_through_review_materializes_post_planning_after_gates() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    fs::write(root.join("sdd.config.yaml"), "approvers:\n  cli: [alan]\n").unwrap();
    assert_success(run(
        root,
        &[
            "demand",
            "--root",
            r,
            "enqueue",
            "--id",
            "DEM-2",
            "--type",
            "story",
            "--title",
            "Fluxo Completo",
        ],
    ));

    assert_success(run(root, &["auto", "--root", r, "run"]));
    assert_success(run(
        root,
        &[
            "demand", "--root", r, "approve", "DEM-2", "--gate", "prd", "--by", "alan",
        ],
    ));
    assert_success(run(root, &["auto", "--root", r, "run"]));
    assert_success(run(
        root,
        &[
            "demand", "--root", r, "approve", "DEM-2", "--gate", "techspec", "--by", "alan",
        ],
    ));
    assert_success(run(root, &["auto", "--root", r, "run"]));
    assert_success(run(
        root,
        &[
            "demand",
            "--root",
            r,
            "approve",
            "DEM-2",
            "--gate",
            "refinement",
            "--by",
            "alan",
        ],
    ));

    let out = run(
        root,
        &["auto", "--root", r, "run", "--through", "review", "--json"],
    );
    let text = combined(&out);
    assert_success(out);
    let parsed: Value = serde_json::from_str(&text).unwrap();
    assert!(parsed["post_planning"]["produced"]
        .as_array()
        .unwrap()
        .iter()
        .any(|item| item[1] == "review"));
    assert!(root.join("docs/fluxo-completo/06-execution.md").is_file());
    assert!(root.join("docs/fluxo-completo/06-adr.md").is_file());
    assert!(root.join("docs/fluxo-completo/07-review.md").is_file());

    let status = run(root, &["demand", "--root", r, "status", "DEM-2"]);
    let status_text = combined(&status);
    assert_success(status);
    assert!(status_text.contains("paused"), "{status_text}");
}

#[test]
fn auto_run_unattended_reaches_memory_without_human_approvers() {
    let t = tempdir().unwrap();
    let root = t.path();
    let r = root.to_str().unwrap();
    assert_success(run(
        root,
        &[
            "demand",
            "--root",
            r,
            "enqueue",
            "--id",
            "DEM-AUTO",
            "--type",
            "story",
            "--title",
            "Fluxo Sem Humano",
        ],
    ));

    let out = run(
        root,
        &["auto", "--root", r, "run", "--unattended", "--json"],
    );
    let text = combined(&out);
    assert_success(out);
    let parsed: Value = serde_json::from_str(&text).unwrap();
    assert_eq!(parsed["unattended"], true);
    assert_eq!(parsed["paused"].as_array().unwrap().len(), 0);
    assert!(
        parsed["auto_accepted"].as_array().unwrap().len() >= 3,
        "{text}"
    );

    for file in [
        "01-idea.md",
        "02-prd.md",
        "03-techspec.md",
        "04-tasks.md",
        "05-refinement.md",
        "06-execution.md",
        "06-adr.md",
        "07-review.md",
        "08-memory.md",
    ] {
        assert!(
            root.join("docs/fluxo-sem-humano").join(file).is_file(),
            "faltou {file}"
        );
    }

    let state_text = fs::read_to_string(root.join(".sdd/state/fluxo-sem-humano.json")).unwrap();
    let state: Value = serde_json::from_str(&state_text).unwrap();
    assert_eq!(state["status"], "completed");
    let approvals = state["approvals"].as_array().unwrap();
    assert_eq!(approvals.len(), 3, "{state_text}");
    assert!(approvals
        .iter()
        .all(|approval| approval["channel"] == "automation"));
    assert!(approvals
        .iter()
        .all(|approval| approval["author"] == "sdd-auto"));

    let audit = fs::read_to_string(root.join(".sdd/orchestrator.jsonl")).unwrap();
    assert!(audit.contains("\"event\":\"gate_auto_accept\""), "{audit}");
}

#[test]
fn providers_memory_and_stage_flags_are_offline_and_redacted() {
    let target = tempdir().unwrap();
    let root = target.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
    let idea_agent = root.join(".codex/agents/idea.toml");
    let idea_agent_text = fs::read_to_string(&idea_agent).unwrap();
    fs::write(
        &idea_agent,
        idea_agent_text.replace("model = \"gpt-5.4\"", "model = \"gpt-5.5\""),
    )
    .unwrap();
    let prd_agent = root.join(".codex/agents/prd.toml");
    let prd_agent_text = fs::read_to_string(&prd_agent).unwrap();
    fs::write(
        &prd_agent,
        prd_agent_text.replace(
            "model = \"gpt-5.4\"",
            "model = \"not-a-real-provider-model\"",
        ),
    )
    .unwrap();

    let providers = combined(&run(root, &["providers", "list", "--json"]));
    assert!(providers.contains("\"id\": \"codex\""));
    assert!(providers.contains("\"id\": \"antigravity\""));
    assert!(!providers.contains("\"id\": \"gemini\""));
    assert!(providers.contains("\"id\": \"opencode\""));
    assert!(providers.contains("\"id\": \"cursor\""));
    assert!(providers.contains("\"id\": \"devin\""));
    assert!(providers.contains("\"id\": \"trae\""));
    assert!(providers.contains("\"id\": \"rig\""));
    assert!(providers.contains("\"kind\": \"rig-adapter\""));
    assert!(providers.contains("\"auth_methods\""));
    assert!(providers.contains("\"models\""));
    assert!(providers.contains("\"efforts\""));
    assert!(providers.contains("\"stage_models\""));
    assert!(providers.contains("\"usage_limits\""));
    assert!(providers.contains("gpt-5.5"));
    assert!(providers.contains("\"idea\": \"gpt-5.5\""));
    assert!(!providers.contains("not-a-real-provider-model"));
    assert!(providers.contains("gemini-3.5-flash"));
    assert!(providers.contains("gemini-3.1-pro"));
    assert!(providers.contains("claude-opus-4.6"));
    assert!(providers.contains("composer-2.5-fast"));
    assert!(providers.contains("composer-2.5"));
    assert!(providers.contains("swe-1-6-fast"));
    assert!(providers.contains("\"sonnet\""));
    assert!(providers.contains("\"opus\""));
    assert!(providers.contains("trae_agent_model"));
    assert!(providers.contains("opencode-go/deepseek-v4-flash"));
    assert!(providers.contains("opencode-go/deepseek-v4-pro"));
    assert!(providers.contains("opencode-go/kimi-k2.7"));
    assert!(providers.contains("opencode-go/qwen3.7-plus"));
    assert!(providers.contains("openai/gpt-5.5"));
    assert!(providers.contains("anthropic/claude-opus-4-8"));
    assert!(providers.contains("provider-routing"));

    let doctor = combined(&run_with_env_values(
        root,
        &["providers", "doctor", "--json"],
        None,
        &[("OPENAI_API_KEY", "sk-test-secret")],
    ));
    assert!(doctor.contains("\"id\": \"codex\""));
    assert!(doctor.contains("Codex CLI"));
    assert!(doctor.contains("OpenAI API"));
    assert!(doctor.contains("Claude Code"));
    assert!(doctor.contains("Antigravity CLI"));
    assert!(doctor.contains("agy models"));
    assert!(doctor.contains("Google AI API"));
    assert!(!doctor.contains("Gemini CLI"));
    assert!(doctor.contains("Cursor CLI"));
    assert!(doctor.contains("Devin CLI"));
    assert!(doctor.contains("devin auth status"));
    assert!(doctor.contains("Trae Agent CLI"));
    assert!(doctor.contains("trae-cli show-config"));
    assert!(doctor.contains("opencode CLI"));
    assert!(doctor.contains("opencode Go/Zen credential"));
    assert!(doctor.contains("\"usage_limits\""));
    assert!(doctor.contains("\"present\": true"));
    assert!(!doctor.contains("sk-test-secret"));

    let dry_run = combined(&run(
        root,
        &[
            "orchestration",
            "--provider",
            "claude",
            "--model",
            "claude-sonnet-4-6",
            "--effort",
            "high",
            "--dry-run",
            "x",
        ],
    ));
    assert!(dry_run.contains("Provider: claude"));
    assert!(dry_run.contains("Model: claude-sonnet-4-6"));
    assert!(dry_run.contains("Effort: high"));
    assert!(dry_run.contains("Offline: true"));

    assert_success(run(
        root,
        &[
            "prd",
            "--provider",
            "custom",
            "--model",
            "local-test",
            "--effort",
            "medium",
            "--name",
            "Ciclo Provider",
            "--force",
            "entrada segura",
        ],
    ));
    let prd = fs::read_to_string(root.join("docs/ciclo-provider/02-prd.md")).unwrap();
    assert!(prd.contains("- Provider: custom"));
    assert!(prd.contains("- Modelo: local-test"));
    assert!(prd.contains("- Effort: medium"));
    assert!(!prd.contains("sk-test-secret"));

    assert_success(run(root, &["memory", "learn", "--name", "Ciclo Provider"]));
    let index = fs::read_to_string(root.join(".sdd/memory/learnings.jsonl")).unwrap();
    assert!(index.contains("\"source_artifact\""));
    assert!(index.contains("\"source_hash\""));
    assert!(index.contains("docs/ciclo-provider/02-prd.md"));

    let status = combined(&run(root, &["memory", "status", "--json"]));
    assert!(status.contains("\"learnings\": 1"));
    assert!(status.contains("\"sources_indexed\": 1"));

    assert_success(run(root, &["doctor"]));
}

#[test]
fn opencode_traceability_uses_stage_routing_and_explicit_overrides() {
    let target = tempdir().unwrap();
    let root = target.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    assert_success(run(
        root,
        &[
            "discover",
            "--provider",
            "opencode",
            "--name",
            "OpenCode Kimi",
            "--force",
            "inventario",
        ],
    ));
    let discovery =
        fs::read_to_string(root.join("docs/opencode-kimi/00-project-discovery.md")).unwrap();
    assert!(discovery.contains("- Provider: opencode"));
    assert!(discovery.contains("- Modelo: opencode-go/kimi-k2.7"));
    assert!(discovery.contains("- Effort: xhigh"));
    assert!(discovery.contains("- Tokens: input ~"));
    assert!(discovery.contains("- Tokens source: estimated"));
    assert!(discovery.contains("- Tempo de geração: "));
    assert!(discovery.contains("- Tempo source: local"));

    let discovery_html =
        fs::read_to_string(root.join("docs/opencode-kimi/00-project-discovery.html")).unwrap();
    assert!(discovery_html.contains("Provider: <code>opencode</code>"));
    assert!(discovery_html.contains("Modelo: <code>opencode-go/kimi-k2.7</code>"));
    assert!(discovery_html.contains("Tokens source: <code>estimated</code>"));
    assert!(discovery_html.contains("Tempo source: <code>local</code>"));

    assert_success(run(
        root,
        &[
            "prd",
            "--provider",
            "opencode",
            "--model",
            "opencode-go/qwen3.7-max",
            "--effort",
            "medium",
            "--name",
            "OpenCode Kimi",
            "--force",
            "ctx",
        ],
    ));
    let prd = fs::read_to_string(root.join("docs/opencode-kimi/02-prd.md")).unwrap();
    assert!(prd.contains("- Provider: opencode"));
    assert!(prd.contains("- Modelo: opencode-go/qwen3.7-max"));
    assert!(prd.contains("- Effort: medium"));
    assert!(prd.contains("- Tokens: input ~"));
    assert!(prd.contains("- Tokens source: estimated"));
    assert!(prd.contains("- Tempo de geração: "));
    assert!(prd.contains("- Tempo source: local"));
    assert!(!prd.contains("- Modelo: opencode-go/deepseek-v4-pro"));
}

#[test]
fn traceability_records_provider_reported_usage_and_timing_from_env() {
    let target = tempdir().unwrap();
    let root = target.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    assert_success(run_with_env_values(
        root,
        &[
            "idea",
            "--provider",
            "opencode",
            "--name",
            "Usage Reported",
            "--force",
            "ctx",
        ],
        None,
        &[
            ("SDD_USAGE_INPUT_TOKENS", "1000"),
            ("SDD_USAGE_CACHED_INPUT_TOKENS", "400"),
            ("SDD_USAGE_OUTPUT_TOKENS", "120"),
            ("SDD_USAGE_REASONING_OUTPUT_TOKENS", "30"),
            ("SDD_USAGE_TOTAL_TOKENS", "1120"),
            ("SDD_GENERATION_STARTED_AT", "2026-06-30T10:00:00Z"),
            (
                "SDD_GENERATION_EXECUTION_STARTED_AT",
                "2026-06-30T10:00:03Z",
            ),
            ("SDD_GENERATION_FINISHED_AT", "2026-06-30T10:00:05Z"),
            ("SDD_GENERATION_DURATION_MS", "5000"),
            ("SDD_GENERATION_REASONING_DURATION_MS", "3000"),
            ("SDD_GENERATION_EXECUTION_DURATION_MS", "2000"),
        ],
    ));

    let idea = fs::read_to_string(root.join("docs/usage-reported/01-idea.md")).unwrap();
    assert!(idea
        .contains("- Tokens: input ~1000 (cache ~400), output ~120 (reasoning ~30), total ~1120"));
    assert!(idea.contains("- Tokens source: provider-reported"));
    assert!(idea
        .contains("- Tempo de geração: 2026-06-30T10:00:00Z -> 2026-06-30T10:00:05Z (~5000 ms)"));
    assert!(idea.contains("- Tempo source: provider-reported"));
    assert!(idea.contains("- Execução iniciada: 2026-06-30T10:00:03Z"));
    assert!(idea.contains("- Tempo fases: reasoning ~3000 ms, execution ~2000 ms"));
}

#[test]
fn artifact_save_injects_provider_reported_telemetry_from_env() {
    let target = tempdir().unwrap();
    let root = target.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
    let input = root.join("external.md");
    fs::write(
        &input,
        "# Idea\n\n## Rastreabilidade\n- Origem: editor externo\n\n## Resumo\n- Conteúdo gerado fora do CLI.\n",
    )
    .unwrap();

    assert_success(run_with_env_values(
        root,
        &[
            "artifact",
            "save",
            "External Timing",
            "idea",
            "--file",
            input.to_str().unwrap(),
            "--state",
            "recorded",
            "--force",
        ],
        None,
        &[
            ("SDD_USAGE_INPUT_TOKENS", "10"),
            ("SDD_USAGE_OUTPUT_TOKENS", "20"),
            ("SDD_USAGE_TOTAL_TOKENS", "30"),
            ("SDD_GENERATION_STARTED_AT", "2026-06-30T11:00:00Z"),
            ("SDD_GENERATION_FINISHED_AT", "2026-06-30T11:00:02Z"),
            ("SDD_GENERATION_DURATION_MS", "2000"),
        ],
    ));

    let saved = fs::read_to_string(root.join("docs/external-timing/01-idea.md")).unwrap();
    assert!(saved.contains("- Tokens: input ~10, output ~20, total ~30"));
    assert!(saved.contains("- Tokens source: provider-reported"));
    assert!(saved
        .contains("- Tempo de geração: 2026-06-30T11:00:00Z -> 2026-06-30T11:00:02Z (~2000 ms)"));
    assert!(saved.contains("- Tempo source: provider-reported"));
    assert!(saved.contains("- Origem: editor externo"));
}

#[test]
fn intelligence_help_lists_learn_status_and_health() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    let help_output = run(root, &["intelligence", "--help"]);
    let help = combined(&help_output);
    assert_success(help_output);
    assert!(help.contains("learn"));
    assert!(help.contains("status"));
    assert!(help.contains("health"));
    assert!(help.contains("repair"));
}

#[test]
fn context_build_write_creates_techspec_pack_with_manifesto() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(
        root,
        &[
            "context", "build", "--name", name, "--stage", "techspec", "--write",
        ],
    ));
    let context_pack_path =
        root.join(".sdd/intelligence/context-packs/project-intelligence-layer/techspec.md");
    let context_pack = fs::read_to_string(&context_pack_path).unwrap();
    assert!(context_pack.contains("Rastreabilidade"));
    assert!(context_pack.contains("Contexto essencial"));
    assert!(context_pack.contains("Manifesto de contexto"));
}

#[test]
fn context_materialize_default_keeps_root_only() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();

    let output = run(root, &["context", "materialize", "--name", name, "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert!(json_contains_text(&json, "src/domain"));
    assert!(json_contains_text(&json, "root-agents"));
    assert_eq!(json["agent_context_policy"], "root_only");
    assert_eq!(json["file_budget"]["planned_local_agents"], 0);
    assert!(json["context_profiles"]
        .as_array()
        .unwrap()
        .iter()
        .any(|profile| {
            profile["path"].as_str() == Some("src/domain")
                && profile["materialized"].as_bool() == Some(false)
        }));
    assert!(!json["actions"]
        .as_array()
        .unwrap()
        .iter()
        .any(|action| { action["target"].as_str() == Some("src/domain/AGENTS.md") }));
    assert!(!root
        .join("docs/project-intelligence-layer/00-context-recommendations.md")
        .exists());
    assert!(!root.join("src/domain/AGENTS.md").exists());
}

#[test]
fn context_materialize_write_updates_managed_blocks_idempotently() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
    let agents = fs::read_to_string(root.join("AGENTS.md")).unwrap();
    fs::write(
        root.join("AGENTS.md"),
        format!("{agents}\n## Manual local\n\nPreservar esta seção.\n"),
    )
    .unwrap();

    assert_success(run(
        root,
        &["context", "materialize", "--name", name, "--write"],
    ));
    assert!(root
        .join("docs/project-intelligence-layer/00-context-recommendations.md")
        .is_file());
    let agents_after = fs::read_to_string(root.join("AGENTS.md")).unwrap();
    assert!(agents_after.contains("## Manual local"));
    assert!(agents_after.contains("Preservar esta seção."));
    assert_eq!(
        agents_after
            .matches("<!-- sdd-context:start id=\"root-agents\" -->")
            .count(),
        1
    );
    let claude = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
    assert_eq!(
        claude
            .matches("<!-- sdd-context:start id=\"root-claude\" -->")
            .count(),
        1
    );
    assert!(!root.join("src/domain/AGENTS.md").exists());

    assert_success(run(
        root,
        &["context", "materialize", "--name", name, "--write"],
    ));
    let agents_second = fs::read_to_string(root.join("AGENTS.md")).unwrap();
    assert_eq!(
        agents_second
            .matches("<!-- sdd-context:start id=\"root-agents\" -->")
            .count(),
        1
    );
    assert!(!root.join("src/domain/AGENTS.md").exists());
}

#[test]
fn context_materialize_local_agents_opt_in_is_capped() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::create_dir_all(root.join("src/runtime")).unwrap();
    fs::create_dir_all(root.join("src/tui")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
    fs::write(root.join("src/runtime/mod.rs"), "pub mod mcp;\n").unwrap();
    fs::write(root.join("src/tui/mod.rs"), "pub mod app;\n").unwrap();

    let output = run(
        root,
        &[
            "context",
            "materialize",
            "--name",
            name,
            "--local-agents",
            "--max-boundaries",
            "2",
            "--write",
            "--json",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert_eq!(json["agent_context_policy"], "scoped_opt_in");
    assert_eq!(json["file_budget"]["max_boundaries"], 2);
    let planned = json["local_agent_boundaries"].as_array().unwrap();
    assert!(planned.len() <= 2);
    assert!(planned.iter().all(|profile| {
        profile["confidence"].as_str() == Some("alta") && profile["path"].as_str() != Some("src")
    }));
    let created = planned
        .iter()
        .filter_map(|profile| profile["path"].as_str())
        .filter(|path| root.join(path).join("AGENTS.md").is_file())
        .count();
    assert_eq!(created, planned.len());
}

#[test]
fn context_materialize_excludes_client_dirs() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join(".opencode")).unwrap();
    fs::write(root.join(".opencode/package.json"), "{}").unwrap();
    fs::create_dir_all(root.join(".cursor")).unwrap();
    fs::write(root.join(".cursor/package.json"), "{}").unwrap();

    let output = run(
        root,
        &[
            "context",
            "materialize",
            "--name",
            name,
            "--local-agents",
            "--json",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert!(!json_contains_text(&json, ".opencode/AGENTS.md"));
    assert!(!json_contains_text(&json, ".cursor/AGENTS.md"));
    assert!(!json["context_profiles"]
        .as_array()
        .unwrap()
        .iter()
        .any(|profile| {
            profile["path"]
                .as_str()
                .is_some_and(|path| path.starts_with(".opencode") || path.starts_with(".cursor"))
        }));
}

#[test]
fn context_materialize_existing_config_local_agents_still_works() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
    let config_path = root.join("sdd.config.yaml");
    let config = fs::read_to_string(&config_path).unwrap();
    let enabled = config.replacen("local_agents: false", "local_agents: true", 1);
    assert_ne!(
        config, enabled,
        "config fixture should include local_agents"
    );
    fs::write(&config_path, enabled).unwrap();

    let output = run(
        root,
        &[
            "context",
            "materialize",
            "--name",
            name,
            "--write",
            "--json",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert_eq!(json["agent_context_policy"], "scoped_config");
    assert!(root.join("src/domain/AGENTS.md").is_file());
}

#[test]
fn mcp_doctor_json_reports_agent_contexts() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["mcp", "doctor", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("mcp doctor output should be json");
    assert!(json.get("agent_contexts").is_some());
    assert_eq!(json["agent_contexts"]["policy"], "root_only");
    assert_eq!(json["agent_contexts"]["file_budget"]["max_boundaries"], 4);
}

#[test]
fn context_materialize_reports_stale_learning_as_blocked_pattern() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    fs::write(
        root.join("docs/project-intelligence-layer/03-techspec.md"),
        "# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    let output = run(root, &["context", "materialize", "--name", name, "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert!(json_contains_text(&json, "Learning obsoleto bloqueado"));
    assert!(json_contains_text(
        &json,
        "docs/project-intelligence-layer/03-techspec.md"
    ));
}

#[test]
fn context_materialize_disabled_does_not_write() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
    let config_path = root.join("sdd.config.yaml");
    let config = fs::read_to_string(&config_path).unwrap();
    let disabled = config.replacen(
        "context_materialization:\n    enabled: true",
        "context_materialization:\n    enabled: false",
        1,
    );
    assert_ne!(
        config, disabled,
        "config fixture should include materialization toggle"
    );
    fs::write(&config_path, disabled).unwrap();

    let output = run(
        root,
        &[
            "context",
            "materialize",
            "--name",
            name,
            "--write",
            "--json",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
    assert!(json_contains_text(
        &json,
        "context materialization disabled"
    ));
    assert!(!root
        .join("docs/project-intelligence-layer/00-context-recommendations.md")
        .exists());
    assert!(!fs::read_to_string(root.join("AGENTS.md"))
        .unwrap()
        .contains("<!-- sdd-context:start id=\"root-agents\" -->"));
    assert!(!root.join("src/domain/AGENTS.md").exists());
}

#[test]
fn context_materialize_plan_includes_expected_block_preview() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("src/domain")).unwrap();
    fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();

    let output = run(root, &["context", "materialize", "--name", name]);
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("## Preview de blocos gerenciados"));
    assert!(text.contains("### `AGENTS.md` / `root-agents`"));
    assert!(text.contains("<!-- sdd-context:start id=\"root-agents\" -->"));
    assert!(!root
        .join("docs/project-intelligence-layer/00-context-recommendations.md")
        .exists());
}

#[test]
fn context_materialize_redacts_persisted_artifact() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join(".sdd/intelligence")).unwrap();
    fs::write(
        root.join(".sdd/intelligence/learnings.jsonl"),
        r#"{"source_artifact":"docs/OPENAI_API_KEY=sk-test-secret.md","source_stage":"memory","status":"active","summary":"Registrar padrão operacional sem segredo."}"#,
    )
    .unwrap();

    assert_success(run(
        root,
        &["context", "materialize", "--name", name, "--write"],
    ));
    let artifact = fs::read_to_string(
        root.join("docs/project-intelligence-layer/00-context-recommendations.md"),
    )
    .unwrap();
    assert!(artifact.contains("OPENAI_API_KEY=[REDACTED]"));
    assert!(!artifact.contains("sk-test-secret"));
}

#[test]
fn context_build_includes_historical_artifacts_from_docs_store() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    assert_success(run(root, &["init", "Legacy Cache Decisions"]));
    fs::write(
        root.join("docs/legacy-cache-decisions/06-adr.md"),
        "# ADR\n\nDecidimos usar snapshots determinísticos para cache operacional.\n",
    )
    .unwrap();

    assert_success(run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "execution",
            "--write",
        ],
    ));
    let context_pack = fs::read_to_string(
        root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
    )
    .unwrap();
    assert!(context_pack.contains("docs/legacy-cache-decisions/06-adr.md"));
    assert!(context_pack.contains("snapshots determinísticos"));
}

#[test]
fn discover_json_reports_domain_code_intelligence_and_does_not_write_on_dry_run() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    fs::write(
        root.join("CONTEXT.md"),
        "# Glossario\n\n- Pedido: demanda.\n",
    )
    .unwrap();
    fs::create_dir_all(root.join("docs/adr")).unwrap();
    fs::write(root.join("docs/adr/0001-cache.md"), "# ADR\n").unwrap();
    fs::write(
        root.join("Cargo.toml"),
        "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
    )
    .unwrap();

    let output = run(root, &["discover", "--json", "--dry-run", "mapear dominio"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
    assert_eq!(
        json.get("artifact_written").and_then(Value::as_bool),
        Some(false)
    );
    assert_eq!(
        json.get("html_artifact_written").and_then(Value::as_bool),
        Some(false)
    );
    assert!(json_contains_text(&json, "CONTEXT.md"));
    assert!(json_contains_text(&json, "docs/adr"));
    assert!(json_contains_text(&json, "codegraph"));
    assert!(json_contains_text(&json, "codegraph init -i ."));
    assert!(json_contains_text(
        &json,
        "codegraph context \"<task>\" --path . --format markdown"
    ));
    assert!(json_contains_text(&json, "lexa"));
    assert!(json_contains_text(&json, "execution-discipline"));
    assert!(!root
        .join("docs/sdd-orchestration/00-project-discovery.md")
        .exists());
    assert!(!root
        .join("docs/sdd-orchestration/00-project-discovery.html")
        .exists());
}

#[test]
fn discover_json_without_name_materializes_fallback_markdown_and_html() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["discover", "--json", "--force"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
    assert_eq!(
        json.get("artifact_written").and_then(Value::as_bool),
        Some(true)
    );
    assert_eq!(
        json.get("html_artifact_written").and_then(Value::as_bool),
        Some(true)
    );
    assert!(json_contains_text(
        &json,
        "docs/sdd-orchestration/00-project-discovery.md"
    ));
    assert!(json_contains_text(
        &json,
        "docs/sdd-orchestration/00-project-discovery.html"
    ));
    assert!(root
        .join("docs/sdd-orchestration/00-project-discovery.md")
        .exists());
    assert!(root
        .join("docs/sdd-orchestration/00-project-discovery.html")
        .exists());
}

#[test]
fn discover_infers_high_level_architecture_from_project_signals() {
    let target = tempdir().unwrap();
    let root = target.path();
    fs::create_dir_all(root.join("app/api/orders")).unwrap();
    fs::create_dir_all(root.join("src/components")).unwrap();
    fs::create_dir_all(root.join("tests")).unwrap();
    fs::create_dir_all(root.join("prisma/migrations/001_init")).unwrap();
    fs::create_dir_all(root.join(".github/workflows")).unwrap();
    fs::write(
        root.join("package.json"),
        r#"{
  "name": "dropchat-frontend",
  "version": "1.2.3",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "test": "vitest run",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "next": "15.0.0",
    "react": "19.0.0",
    "stripe": "17.0.0",
    "next-auth": "5.0.0",
    "@sentry/nextjs": "8.0.0",
    "@prisma/client": "6.0.0",
    "bullmq": "5.0.0"
  },
  "devDependencies": {
    "vitest": "2.0.0",
    "typescript": "5.0.0"
  }
}"#,
    )
    .unwrap();
    fs::write(
        root.join("app/page.tsx"),
        "export default function Page() { return null }\n",
    )
    .unwrap();
    fs::write(
        root.join("app/api/orders/route.ts"),
        "export async function POST() { return Response.json({ ok: true }) }\n",
    )
    .unwrap();
    fs::write(
        root.join("src/components/Button.tsx"),
        "export function Button() {}\n",
    )
    .unwrap();
    fs::write(
        root.join("tests/orders.test.ts"),
        "import { test } from 'vitest'\n",
    )
    .unwrap();
    fs::write(
        root.join("prisma/schema.prisma"),
        "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n",
    )
    .unwrap();
    fs::write(
        root.join("prisma/migrations/001_init/migration.sql"),
        "-- init\n",
    )
    .unwrap();
    fs::write(root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
    fs::write(root.join("Dockerfile"), "FROM node:22\n").unwrap();
    fs::write(root.join("vercel.json"), "{}\n").unwrap();
    fs::write(
        root.join(".env.example"),
        "DATABASE_URL=postgres://example\nSTRIPE_SECRET_KEY=sk_test_secret\nSENTRY_DSN=https://example\n",
    )
    .unwrap();

    let output = run(root, &["discover", "--json", "--dry-run", "onboarding"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
    assert!(json_contains_text(&json, "dropchat-frontend"));
    assert!(json_contains_text(&json, "app/page.tsx"));
    assert!(json_contains_text(&json, "app/api"));
    assert!(json_contains_text(&json, "Next.js"));
    assert!(json_contains_text(&json, "Prisma"));
    assert!(json_contains_text(&json, "Stripe"));
    assert!(json_contains_text(&json, "Sentry"));
    assert!(json_contains_text(&json, "BullMQ"));
    assert!(json_contains_text(&json, "DATABASE_URL"));
    assert!(json_contains_text(&json, "STRIPE_SECRET_KEY"));
    assert!(!json_contains_text(&json, "sk_test_secret"));
}

#[test]
fn discover_named_artifact_contains_capability_map_and_code_intelligence() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    assert_success(run(
        root,
        &[
            "discover",
            "--name",
            "Discovery Intelligence",
            "--force",
            "brownfield execution",
        ],
    ));
    let artifact =
        fs::read_to_string(root.join("docs/discovery-intelligence/00-project-discovery.md"))
            .unwrap();
    let html =
        fs::read_to_string(root.join("docs/discovery-intelligence/00-project-discovery.html"))
            .unwrap();
    assert!(artifact.contains("## Arquitetura e padrões"));
    assert!(artifact.contains("## Inventário técnico detalhado"));
    assert!(artifact.contains("Resumo inferido"));
    assert!(artifact.contains("Entrypoints reais"));
    assert!(artifact.contains("## Visualizações e documentação HTML"));
    assert!(artifact.contains("```mermaid"));
    assert!(artifact.contains("00-project-discovery.html"));
    assert!(artifact.contains("Code intelligence"));
    assert!(artifact.contains("codegraph init -i ."));
    assert!(artifact.contains("Comandos sugeridos"));
    assert!(artifact.contains("code-intelligence"));
    assert!(artifact.contains("execution-discipline"));
    assert!(artifact.contains("domain-language"));
    assert!(artifact.contains("architecture-deepening"));
    assert!(artifact.contains("Fallback"));
    assert!(html.contains("<title>Project Discovery - Discovery Intelligence</title>"));
    assert!(html.contains("Stack Utilizada"));
    assert!(html.contains("Arquitetura Inferida"));
    assert!(html.contains("Manifests e Dependências"));
    assert!(html.contains("Cobertura do Inventário"));
    assert!(html.contains("Fluxo SDD Recomendado"));
    assert!(html.contains("Capability Map"));
}

#[test]
fn optimize_status_json_reports_optional_wrappers_and_ignored_paths() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["optimize", "status", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("status output should be json");
    assert_eq!(json.get("enabled").and_then(Value::as_bool), Some(true));
    assert!(json_contains_text(&json, "codegraph"));
    assert!(json_contains_text(&json, "rtk"));
    assert!(json_contains_text(&json, "caveman"));
    assert!(json_contains_text(&json, "node_modules"));
    assert!(json_contains_text(&json, "nova hipótese testada"));
    assert!(json_contains_text(&json, "mesmo comando"));
}

#[test]
fn context_build_metadata_only_omits_content_and_returns_handoff() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    create_project_intelligence_artifacts(root);

    let output = run(
        root,
        &[
            "context",
            "build",
            "--name",
            "Project Intelligence Layer",
            "--stage",
            "execution",
            "--json",
            "--metadata-only",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("metadata should be json");
    assert!(json.get("content").is_none(), "{text}");
    assert!(json.get("path").and_then(Value::as_str).is_some());
    assert!(
        json.get("sources_included")
            .and_then(Value::as_u64)
            .unwrap_or_default()
            > 0
    );
    assert!(json.get("conflicts").and_then(Value::as_u64).is_some());
    assert!(json.get("used_chars").and_then(Value::as_u64).is_some());
    assert!(json
        .pointer("/handoff/strategy")
        .and_then(Value::as_str)
        .is_some());
    assert!(json
        .pointer("/handoff/suggested_tests")
        .and_then(Value::as_array)
        .is_some());
    assert!(json.get("stale").and_then(Value::as_array).is_some());
    assert!(json
        .get("decision_conflicts")
        .and_then(Value::as_array)
        .is_some());
}

#[test]
fn context_pack_execution_prioritizes_handoff_before_long_artifact_snippets() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    create_project_intelligence_artifacts(root);
    fs::write(
        root.join("Cargo.toml"),
        "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
    )
    .unwrap();

    let output = run(
        root,
        &[
            "context",
            "build",
            "--name",
            "Project Intelligence Layer",
            "--stage",
            "execution",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let handoff_index = text.find("## Context Handoff").expect("handoff section");
    let decisions_index = text
        .find("## Decisões relevantes")
        .expect("decisions section");
    assert!(
        handoff_index < decisions_index,
        "handoff must appear before long snippets"
    );
    assert!(
        text.contains("- `cargo test`") || text.contains("descobrir pelo manifesto"),
        "{text}"
    );
    assert!(text.contains("Fallback amplo somente quando o handoff não cobrir o risco"));
}

#[test]
fn health_json_reports_pass_and_codegraph_absence_without_orchestration_name() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let config_path = root.join("sdd.config.yaml");
    let config = fs::read_to_string(&config_path).unwrap();
    fs::write(
        &config_path,
        config.replace("command: codegraph", "command: missing-codegraph"),
    )
    .unwrap();

    let output = run(root, &["health", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("health should be json");
    assert_eq!(json["status"], "pass", "{text}");
    assert_eq!(json["components"]["quality"]["status"], "skipped");
    assert_eq!(
        json["components"]["optimization"]["codegraph"]["available"],
        false
    );
    assert_eq!(json["components"]["workflow"]["status"], "pass");
    assert_eq!(json["components"]["clients"]["status"], "pass");
    assert_eq!(json["components"]["skills"]["status"], "pass");
}

#[test]
fn health_json_surfaces_missing_memory_warnings() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    assert_success(run(root, &["init", "Health Warning"]));

    let output = run(root, &["health", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("health should be json");
    assert_eq!(json["status"], "warn", "{text}");
    assert_eq!(json["components"]["intelligence"]["status"], "warn");
    assert!(json_contains_text(&json, "missing-memory"), "{text}");
}

#[test]
fn health_json_keeps_info_only_intelligence_signals_passing() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    assert_success(run(root, &["init", "Info Only"]));
    fs::write(
        root.join("docs/info-only/08-memory.md"),
        "# Memória\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    let output = run(root, &["health", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("health should be json");
    assert_eq!(json["status"], "pass", "{text}");
    assert_eq!(json["components"]["intelligence"]["status"], "pass");
    assert_eq!(json["components"]["intelligence"]["warning_count"], 0);
    assert!(json_contains_text(&json, "missing-artifact"), "{text}");
    assert!(json_contains_text(
        &json,
        "sdd intelligence repair --all --json"
    ));
}

#[test]
fn health_strict_fails_on_warning_status() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    assert_success(run(root, &["init", "Health Warning"]));

    let output = run(root, &["health", "--strict", "--name", "health-warning"]);
    assert!(
        !output.status.success(),
        "expected health strict to fail\n{}",
        combined(&output)
    );
}

#[test]
fn ci_strict_release_dry_run_json_reports_gate_matrix() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(
        root,
        &[
            "ci",
            "--strict-release",
            "--dry-run",
            "--json",
            "--name",
            "Release Gate",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
    assert_eq!(json["dry_run"], true);
    assert!(json["checks"].as_array().unwrap().iter().any(|check| {
        check["command"]
            .as_str()
            .is_some_and(|command| command == "cargo fmt --check")
    }));
    assert!(json["checks"].as_array().unwrap().iter().any(|check| {
        check["command"]
            .as_str()
            .is_some_and(|command| command == "sdd health --strict --json --name Release Gate")
    }));
    assert!(json["checks"]
        .as_array()
        .unwrap()
        .iter()
        .any(|check| { check["area"].as_str() == Some("advisory") && check["required"] == false }));

    let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
    assert!(json["checks"].as_array().unwrap().iter().any(|check| {
        check["command"]
            .as_str()
            .is_some_and(|command| command == "sdd health --strict --json")
    }));
}

#[test]
fn ci_strict_release_markdown_is_human_friendly() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["ci", "--strict-release", "--dry-run"]);
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("# SDD Strict Release Gate"));
    assert!(text.contains("- Leitura: dry-run"));
    assert!(text.contains("## Resumo"));
    assert!(text.contains("## Pendências"));
    assert!(text.contains("<summary>Ver matriz completa de checks</summary>"));
    assert!(text.contains("| Área | Obrigatórios | Opcionais | Sinal |"));
    assert!(text.contains("Dry-run apenas planejou os checks"));
}

#[test]
fn ci_strict_release_without_name_still_uses_strict_health() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
    assert!(json["checks"].as_array().unwrap().iter().any(|check| {
        check["command"]
            .as_str()
            .is_some_and(|command| command == "sdd health --strict --json")
    }));
}

#[test]
fn ci_strict_release_uses_layer_source_for_core_checks_from_installed_project() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    assert!(!root.join("Cargo.toml").exists());

    let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
    let checks = json["checks"].as_array().unwrap();
    let expected_project_root = fs::canonicalize(root).unwrap().display().to_string();
    let expected_release_root = fs::canonicalize(repo_root()).unwrap().display().to_string();

    assert_eq!(json["root"].as_str().unwrap(), expected_project_root);
    assert_eq!(
        json["release_root"].as_str().unwrap(),
        expected_release_root
    );

    let cargo_fmt = checks
        .iter()
        .find(|check| check["command"].as_str() == Some("cargo fmt --check"))
        .expect("cargo fmt check");
    assert_eq!(cargo_fmt["cwd"].as_str().unwrap(), expected_release_root);

    let health = checks
        .iter()
        .find(|check| check["command"].as_str() == Some("sdd health --strict --json"))
        .expect("health check");
    assert_eq!(health["cwd"].as_str().unwrap(), expected_project_root);
}

#[test]
fn maintenance_report_json_covers_expected_areas() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);

    let output = run(root, &["maintenance", "report", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("maintenance report should be json");
    let ids = json["areas"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|area| area["id"].as_str())
        .collect::<Vec<_>>();
    for expected in [
        "core-rust",
        "docs",
        "clients",
        "bot",
        "providers",
        "intelligence",
        "workflows",
        "release",
        "diagrams",
        "advisory",
    ] {
        assert!(ids.contains(&expected), "missing area {expected}: {text}");
    }
    let area_status = |id: &str| {
        json["areas"]
            .as_array()
            .unwrap()
            .iter()
            .find(|area| area["id"].as_str() == Some(id))
            .and_then(|area| area["status"].as_str())
            .unwrap()
            .to_string()
    };
    assert_eq!(area_status("core-rust"), "skipped");
    assert_eq!(area_status("bot"), "skipped");
    assert_eq!(area_status("release"), "skipped");
}

#[test]
fn maintenance_report_marks_presence_only_areas_as_skipped() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    fs::write(root.join("Cargo.toml"), "[package]\nname = \"fixture\"\n").unwrap();
    fs::create_dir_all(root.join("docs-site")).unwrap();
    fs::write(root.join("docs-site/package.json"), "{}").unwrap();
    fs::create_dir_all(root.join("bot")).unwrap();
    fs::write(root.join("bot/package.json"), "{}").unwrap();

    let output = run(root, &["maintenance", "report", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("maintenance report should be json");
    let area_status = |id: &str| {
        json["areas"]
            .as_array()
            .unwrap()
            .iter()
            .find(|area| area["id"].as_str() == Some(id))
            .and_then(|area| area["status"].as_str())
            .unwrap_or("")
            .to_string()
    };
    assert_eq!(area_status("core-rust"), "skipped");
    assert_eq!(area_status("docs"), "skipped");
    assert_eq!(area_status("bot"), "skipped");
    assert_eq!(area_status("release"), "skipped");
}

#[test]
fn optimize_compress_dedupes_trace_and_preserves_operational_identifiers() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    fs::write(
        root.join("trace.log"),
        "cmd: cargo test\ncmd: cargo test\nfile: src/main.rs\nurl: https://example.com/a\nTask T-01\nhash abc123def456\n",
    )
    .unwrap();

    let output = run(
        root,
        &[
            "optimize",
            "compress",
            "--kind",
            "trace",
            "--file",
            "trace.log",
        ],
    );
    let text = combined(&output);
    assert_success(output);
    assert!(text.contains("Caveman trace summary"));
    assert!(text.contains("cargo test"));
    assert!(text.contains("src/main.rs"));
    assert!(text.contains("https://example.com/a"));
    assert!(text.contains("T-01"));
    assert!(text.contains("abc123def456"));
    assert_eq!(text.matches("cargo test").count(), 1);
}

#[test]
fn context_build_rejects_invalid_stage_path() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    let output = run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "../escape",
            "--write",
        ],
    );
    assert_code(output, 1);
    assert!(!root
        .join(".sdd/intelligence/context-packs/escape.md")
        .exists());
}

#[test]
fn intelligence_learn_writes_enriched_jsonl_and_memory_learn_stays_compatible() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    let sources_index = fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl"))
        .unwrap()
        .lines()
        .filter(|line| !line.trim().is_empty())
        .count();
    let records = intelligence_index
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
        .collect::<Vec<_>>();
    assert!(!records.is_empty());
    assert!(records.iter().any(|record| {
        record
            .get("source_artifact")
            .and_then(Value::as_str)
            .is_some_and(|path| path == "docs/project-intelligence-layer/03-techspec.md")
    }));
    for record in &records {
        assert!(record
            .get("source_artifact")
            .and_then(Value::as_str)
            .is_some());
        assert!(record.get("source_hash").and_then(Value::as_str).is_some());
        assert!(record.get("status").and_then(Value::as_str).is_some());
        assert_eq!(record.get("derived").and_then(Value::as_bool), Some(true));
    }
    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    let sources_index_after_second_learn =
        fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl"))
            .unwrap()
            .lines()
            .filter(|line| !line.trim().is_empty())
            .count();
    assert_eq!(sources_index_after_second_learn, sources_index);

    assert_success(run(root, &["memory", "learn", "--name", name]));
    let memory_index = fs::read_to_string(root.join(".sdd/memory/learnings.jsonl")).unwrap();
    assert!(memory_index.contains("\"source_artifact\""));
    assert!(memory_index.contains("\"source_hash\""));
    assert!(memory_index.contains("docs/project-intelligence-layer/03-techspec.md"));
}

#[test]
fn intelligence_learn_indexes_memory_when_present() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::write(
        root.join("docs/project-intelligence-layer/08-memory.md"),
        "# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- teste\n\n## Decisões\n- Estratégia de cache: JSONL derivado\n\n## Padrões úteis\n- Context Pack.\n\n## Testes e comandos\n- cargo test\n\n## Pendências\n- Nenhuma.\n\n## Contexto para o próximo agente\nUse docs.\n",
    )
    .unwrap();

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    assert!(intelligence_index.contains("\"source_stage\":\"memory\""));
    assert!(intelligence_index.contains("docs/project-intelligence-layer/08-memory.md"));
}

#[test]
fn intelligence_repair_writes_operational_memory_and_refreshes_stale_learning() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    fs::write(
        root.join("docs/project-intelligence-layer/03-techspec.md"),
        "# Tech Spec alterada\n\n## Decisões\n- Motor de memória operacional: gerar `08-memory.md` derivado\n\n## Falhas recorrentes\n- stale-learning aparece quando o hash muda\n\n## Comandos confiáveis\n- `cargo test --locked --test cli`\n\n## Padrões por repo\n- `docs/<slug>/traceability-map.yaml` é a fonte canônica local\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    let output = run(root, &["intelligence", "repair", "--name", name, "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("repair output should be json");
    assert_eq!(json["status"], "pass", "{text}");
    assert!(json_contains_text(
        &json,
        "docs/project-intelligence-layer/08-memory.md"
    ));
    assert_eq!(json["remaining_operational_signals"], 0);

    let memory = fs::read_to_string(root.join("docs/project-intelligence-layer/08-memory.md"))
        .expect("repair should write operational memory");
    assert!(memory.contains("## Estado final"));
    assert!(memory.contains("## Decisões"));
    assert!(memory.contains("## Falhas recorrentes"));
    assert!(memory.contains("## Comandos confiáveis"));
    assert!(memory.contains("## Padrões úteis"));
    assert!(memory.contains("## Testes e comandos"));
    assert!(memory.contains("## Contexto para o próximo agente"));
    assert!(memory.contains("cargo test --locked --test cli"));
    assert!(memory.contains("traceability-map.yaml"));

    let health_output = run(root, &["intelligence", "health", "--json"]);
    let health_text = combined(&health_output);
    assert_success(health_output);
    let health = serde_json::from_str::<Value>(&health_text).expect("health output should be json");
    assert!(
        !json_contains_text(&health, "missing-memory"),
        "{health_text}"
    );
    assert!(
        !json_contains_text(&health, "stale-learning"),
        "{health_text}"
    );

    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    let records = intelligence_index
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
        .collect::<Vec<_>>();
    assert!(records.iter().any(|record| {
        record.get("source_artifact").and_then(Value::as_str)
            == Some("docs/project-intelligence-layer/03-techspec.md")
            && record.get("status").and_then(Value::as_str) == Some("active")
    }));
    assert!(records.iter().any(|record| {
        record.get("source_artifact").and_then(Value::as_str)
            == Some("docs/project-intelligence-layer/08-memory.md")
            && record.get("source_stage").and_then(Value::as_str) == Some("memory")
    }));
}

#[test]
fn intelligence_learn_all_indexes_every_traceable_docs_store() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    create_project_intelligence_artifacts(root);
    assert_success(run(root, &["init", "Legacy Cache Decisions"]));
    fs::write(
        root.join("docs/legacy-cache-decisions/03-techspec.md"),
        "# Tech Spec Legacy\n\n## Decisões\n- Estratégia de cache: snapshots determinísticos\n",
    )
    .unwrap();

    let output = run(root, &["intelligence", "learn", "--all", "--json"]);
    let text = combined(&output);
    assert_success(output);
    let json = serde_json::from_str::<Value>(&text).expect("learn output should be json");
    assert_eq!(json.get("scope").and_then(Value::as_str), Some("all"));
    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    let sources_index =
        fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl")).unwrap();
    assert!(intelligence_index.contains("docs/project-intelligence-layer/03-techspec.md"));
    assert!(intelligence_index.contains("docs/legacy-cache-decisions/03-techspec.md"));
    assert!(sources_index.contains("docs/project-intelligence-layer/traceability-map.yaml"));
    assert!(sources_index.contains("docs/legacy-cache-decisions/traceability-map.yaml"));
}

#[test]
fn intelligence_status_json_marks_changed_learning_as_stale() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    fs::write(
        root.join("docs/project-intelligence-layer/03-techspec.md"),
        "# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    let status_output = run(root, &["intelligence", "status", "--json"]);
    let status_text = combined(&status_output);
    assert_success(status_output);
    let status =
        serde_json::from_str::<Value>(&status_text).expect("status output should be valid json");
    assert!(status
        .get("stale_sources")
        .and_then(Value::as_u64)
        .is_some_and(|count| count >= 1));

    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    let records = intelligence_index
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
        .collect::<Vec<_>>();
    assert!(records.iter().any(|record| {
        record.get("source_artifact").and_then(Value::as_str)
            == Some("docs/project-intelligence-layer/03-techspec.md")
            && record.get("status").and_then(Value::as_str) == Some("stale")
    }));
}

#[test]
fn context_build_dry_run_does_not_persist_stale_status() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    fs::write(
        root.join("docs/project-intelligence-layer/03-techspec.md"),
        "# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    assert_success(run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "execution",
            "--write",
            "--dry-run",
        ],
    ));
    let intelligence_index =
        fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
    let records = intelligence_index
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
        .collect::<Vec<_>>();
    assert!(records.iter().any(|record| {
        record.get("source_artifact").and_then(Value::as_str)
            == Some("docs/project-intelligence-layer/03-techspec.md")
            && record.get("status").and_then(Value::as_str) == Some("active")
    }));
}

#[test]
fn context_build_reports_decision_conflict_with_stage_precedence() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::write(
        root.join("adr-input.md"),
        "# ADR\n\n## Decisão\n- Estratégia de cache: JSONL derivado\n",
    )
    .unwrap();
    fs::write(
        root.join("memory-input.md"),
        "# Memória\n\n## Decisões\n- Estratégia de cache: SQLite como fonte canônica\n",
    )
    .unwrap();
    assert_success(run(
        root,
        &[
            "artifact",
            "save",
            name,
            "adr",
            "--file",
            "adr-input.md",
            "--state",
            "approved",
        ],
    ));
    assert_success(run(
        root,
        &[
            "artifact",
            "save",
            name,
            "memory",
            "--file",
            "memory-input.md",
            "--state",
            "approved",
        ],
    ));

    assert_success(run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "execution",
            "--write",
        ],
    ));
    let context_pack = fs::read_to_string(
        root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
    )
    .unwrap();
    assert!(context_pack.contains("Conflito de decisão"));
    assert!(context_pack.contains("06-adr.md"));
    assert!(context_pack.contains("08-memory.md"));
    assert!(context_pack.contains("tem precedência"));
}

#[test]
fn context_build_detects_prose_and_table_decision_conflicts() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::write(
        root.join("adr-input.md"),
        "# ADR\n\n## Rastreabilidade\n- teste\n\n## Status\nAprovada.\n\n## Contexto\nDecidimos usar JSONL derivado para cache operacional.\n\n## Problema\nEvitar cache opaco.\n\n## Decisão\nRegistrar a política no fluxo SDD.\n\n## Alternativas consideradas\n- SQLite.\n\n## Consequências\n- Índice reconstruível.\n\n## Impactos e riscos\n- Baixo.\n\n## Plano de adoção\n- Usar CLI.\n\n## Plano de reversão\n- Remover índice.\n\n## Evidências\n- teste\n\n## Histórico de revisão\n- inicial\n",
    )
    .unwrap();
    fs::write(
        root.join("memory-input.md"),
        "# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- teste\n\n## Decisões\n| Decisão | Escolha |\n|---|---|\n| Cache operacional | SQLite canônico |\n\n## Padrões úteis\n- Context Pack.\n\n## Testes e comandos\n- cargo test\n\n## Pendências\n- Nenhuma.\n\n## Contexto para o próximo agente\nUse docs.\n",
    )
    .unwrap();
    assert_success(run(
        root,
        &[
            "artifact",
            "save",
            name,
            "adr",
            "--file",
            "adr-input.md",
            "--state",
            "approved",
        ],
    ));
    assert_success(run(
        root,
        &[
            "artifact",
            "save",
            name,
            "memory",
            "--file",
            "memory-input.md",
            "--state",
            "approved",
        ],
    ));

    assert_success(run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "execution",
            "--write",
        ],
    ));
    let context_pack = fs::read_to_string(
        root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
    )
    .unwrap();
    assert!(context_pack.contains("Conflito de decisão `cache-operacional`"));
    assert!(context_pack.contains("06-adr.md"));
    assert!(context_pack.contains("08-memory.md"));
}

#[test]
fn context_build_execution_pack_contains_code_intelligence_and_capability_handoff() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);
    fs::create_dir_all(root.join("node_modules/pkg")).unwrap();
    fs::write(root.join("node_modules/pkg/ignored.md"), "IGNORE-ME").unwrap();
    fs::create_dir_all(root.join("target/debug")).unwrap();
    fs::write(root.join("target/debug/ignored.md"), "IGNORE-ME").unwrap();

    assert_success(run(
        root,
        &[
            "context",
            "build",
            "--name",
            name,
            "--stage",
            "execution",
            "--task",
            "T-04",
            "--write",
        ],
    ));
    let context_pack = fs::read_to_string(
        root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
    )
    .unwrap();
    assert!(context_pack.contains("## Context Handoff"));
    assert!(context_pack.contains("Task: `T-04`"));
    assert!(context_pack.contains("Fora de escopo por padrão"));
    assert!(context_pack.contains("Loop produtivo"));
    assert!(context_pack.contains("Optimization wrapper"));
    assert!(context_pack.contains("rtk wrapper"));
    assert!(context_pack.contains("caveman wrapper"));
    assert!(context_pack.contains("## Code intelligence"));
    assert!(context_pack.contains("codegraph"));
    assert!(context_pack.contains("codegraph init -i ."));
    assert!(context_pack.contains("codegraph context \"<task>\" --path . --format markdown"));
    assert!(context_pack.contains("lexa"));
    assert!(context_pack.contains("## Capability handoff"));
    assert!(context_pack.contains("execution-discipline"));
    assert!(context_pack.contains("code-intelligence"));
    assert!(context_pack.contains("tracer bullet vertical"));
    assert!(!context_pack.contains("IGNORE-ME"));
}

#[test]
fn intelligence_health_json_reports_missing_memory_for_incomplete_orchestration() {
    let incomplete = tempdir().unwrap();
    let incomplete_root = incomplete.path();
    install_generic_project(incomplete_root);
    assert_success(run(incomplete_root, &["init", "Incomplete Intelligence"]));
    let health_output = run(incomplete_root, &["intelligence", "health", "--json"]);
    let health_text = combined(&health_output);
    assert_success(health_output);
    let health =
        serde_json::from_str::<Value>(&health_text).expect("health output should be valid json");
    assert!(json_contains_text(&health, "memory"));
    assert!(
        json_contains_text(&health, "missing")
            || json_contains_text(&health, "ausente")
            || json_contains_text(&health, "absent"),
        "expected missing memory signal in health json:\n{health_text}"
    );
}

#[test]
fn intelligence_health_fail_on_threshold_returns_nonzero() {
    let incomplete = tempdir().unwrap();
    let incomplete_root = incomplete.path();
    install_generic_project(incomplete_root);
    assert_success(run(incomplete_root, &["init", "Incomplete Intelligence"]));

    let output = run(
        incomplete_root,
        &["intelligence", "health", "--fail-on", "warning"],
    );
    let text = combined(&output);
    assert_code(output, 1);
    assert!(text.contains("missing-memory"));
    assert!(text.contains("intelligence health failed"));
}

#[test]
fn intelligence_health_detects_stale_learning_without_status_first() {
    let target = tempdir().unwrap();
    let root = target.path();
    install_generic_project(root);
    let name = "Project Intelligence Layer";
    create_project_intelligence_artifacts(root);

    assert_success(run(root, &["intelligence", "learn", "--name", name]));
    fs::write(
        root.join("docs/project-intelligence-layer/03-techspec.md"),
        "# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
    )
    .unwrap();

    let health_output = run(root, &["intelligence", "health", "--json"]);
    let health_text = combined(&health_output);
    assert_success(health_output);
    let health =
        serde_json::from_str::<Value>(&health_text).expect("health output should be valid json");
    assert!(json_contains_text(&health, "stale-learning"));
    assert!(json_contains_text(&health, "hash mudou"));
}

#[test]
fn creation_templates_match_cli_stage_contract() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();

    assert_success(run(root, &["init", "Fluxo Completo"]));

    let contract = fs::read_to_string(repo_root().join("schemas/sdd-contract.yaml"))
        .expect("contract manifest");
    let expected = contract_stage_files(&contract);
    let generated = fs::read_to_string(root.join("docs/fluxo-completo/traceability-map.yaml"))
        .expect("generated traceability map");
    let generated_stages = artifact_stage_files(&generated);
    assert_eq!(generated_stages, expected);

    for rel in [
        "templates/traceability-map.yaml",
        "templates/artifact-index.yaml",
    ] {
        let template = fs::read_to_string(repo_root().join(rel)).expect(rel);
        assert_eq!(
            artifact_stage_files(&template),
            generated_stages,
            "{rel} drifted from the CLI artifact stage contract"
        );
    }
}

#[test]
fn portable_orchestration_clients_are_generated_from_canonical_skill_source() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();

    assert_success(run(
        root,
        &[
            "clients",
            "sync",
            "--targets",
            "codex,antigravity,cursor,devin",
            "--force",
        ],
    ));

    let canonical = canonical_orchestration_skill();
    assert_portable_orchestration_skill_contract(canonical);
    for rel in [
        ".agents/skills/orchestration/SKILL.md",
        ".cursor/skills/orchestration/SKILL.md",
        ".devin/skills/orchestration/SKILL.md",
    ] {
        let generated = fs::read_to_string(root.join(rel)).unwrap();
        assert_eq!(
            generated, canonical,
            "{rel} drifted from templates/client-skills/orchestration.md"
        );
    }
}

#[test]
fn artifact_store_and_stage_pipeline_work_end_to_end() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();

    assert_success(run(
        root,
        &["artifact", "init", "Minha Orquestração", "--dry-run"],
    ));
    assert!(!root.join("docs").exists());
    assert_success(run(root, &["init", "Minha Orquestração"]));

    for (stage, input) in [
        ("discover", "projeto brownfield"),
        ("risk", "exportação csv com baixo risco"),
        ("idea", "exportar csv"),
        ("prd", "visão aprovada"),
        ("techspec", "arquitetura aprovada"),
        ("tasks", "backlog técnico"),
        ("refinement", "grooming necessário"),
        ("execution", "TASK-001"),
        ("adr", "decisão arquitetural pós-execução"),
        ("review", "diff atual"),
        ("memory", "feature concluída"),
    ] {
        assert_success(run(
            root,
            &[stage, "--name", "Minha Orquestração", "--force", input],
        ));
    }
    assert_success(run(
        root,
        &[
            "orchestration",
            "--name",
            "Minha Orquestração",
            "--force",
            "exportar csv",
        ],
    ));
    assert_success(run(
        root,
        &[
            "orchestrator",
            "--name",
            "Minha Orquestração",
            "--force",
            "exportar csv",
        ],
    ));

    let status = run(root, &["artifact", "status", "Minha Orquestração"]);
    assert_success(status);
    let text = combined(&run(root, &["artifact", "status", "Minha Orquestração"]));
    for stage in [
        "project-discovery",
        "risk",
        "idea",
        "prd",
        "techspec",
        "tasks",
        "refinement",
        "execution",
        "adr",
        "review",
        "memory",
    ] {
        assert!(
            text.contains(&format!("{stage}: ok")),
            "{stage} missing\n{text}"
        );
    }

    let input = root.join("execution.md");
    fs::write(&input, "TASK-002\n").unwrap();
    assert_success(run(
        root,
        &[
            "artifact",
            "save",
            "Minha Orquestração",
            "execution",
            "--file",
            input.to_str().unwrap(),
            "--state",
            "recorded",
            "--append",
        ],
    ));
    let execution =
        fs::read_to_string(root.join("docs/minha-orquestracao/06-execution.md")).unwrap();
    assert!(execution.contains("TASK-001"));
    assert!(execution.contains("TASK-002"));

    let schema = repo_root().join("schemas/artifact-sections.json");
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "risk",
            "docs/minha-orquestracao/00-risk-classification.md",
            "--schema",
            schema.to_str().unwrap(),
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "adr",
            "docs/minha-orquestracao/06-adr.md",
            "--schema",
            schema.to_str().unwrap(),
        ],
    ));
}

#[test]
fn artifact_link_registers_typed_github_external_link() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();

    assert_success(run(root, &["artifact", "init", "GitHub Flow"]));
    assert_success(run(
        root,
        &[
            "artifact",
            "link",
            "GitHub Flow",
            "github",
            "--url",
            "https://github.com/acme/api/issues/42",
            "--kind",
            "issue",
            "--id",
            "GH-42",
            "--stage",
            "idea",
        ],
    ));

    let map = fs::read_to_string(root.join("docs/github-flow/traceability-map.yaml")).unwrap();
    let yaml: YamlValue = serde_yaml::from_str(&map).unwrap();
    let root_map = yaml.as_mapping().unwrap();
    let github = yaml_get(root_map, "external_links")
        .and_then(YamlValue::as_mapping)
        .and_then(|links| yaml_get(links, "github"))
        .and_then(YamlValue::as_sequence)
        .unwrap();
    let first = github[0].as_mapping().unwrap();

    assert_eq!(
        yaml_get(first, "url").and_then(YamlValue::as_str),
        Some("https://github.com/acme/api/issues/42")
    );
    assert_eq!(
        yaml_get(first, "kind").and_then(YamlValue::as_str),
        Some("issue")
    );
    assert_eq!(
        yaml_get(first, "id").and_then(YamlValue::as_str),
        Some("GH-42")
    );
    assert_eq!(
        yaml_get(first, "stage").and_then(YamlValue::as_str),
        Some("idea")
    );
}

#[test]
fn artifact_link_dry_run_does_not_create_store() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();

    assert_success(run(
        root,
        &[
            "artifact",
            "link",
            "Dry Run Link",
            "github",
            "--url",
            "https://github.com/acme/api/issues/42",
            "--dry-run",
        ],
    ));

    assert!(
        !root.join("docs/dry-run-link").exists(),
        "artifact link --dry-run must not create docs/<slug>"
    );
}

#[test]
fn clients_and_context_commands_cover_supported_surfaces() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    fs::write(
        root.join("package.json"),
        r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
    )
    .unwrap();

    assert_success(run(
        repo_root(),
        &[
            "install",
            root.to_str().unwrap(),
            "--preset",
            "frontend-react",
        ],
    ));
    let list = combined(&run(root, &["clients", "list"]));
    assert!(list.contains("opencode"));
    assert!(list.contains("antigravity"));
    assert!(list.contains("devin"));
    assert!(list.contains("trae"));
    assert_success(run(root, &["clients", "doctor"]));

    let codex_loop = fs::read_to_string(root.join(".codex/commands/agentic-sdd-loop.md")).unwrap();
    assert!(codex_loop.contains("agent: sdd-orchestrator"));
    assert!(codex_loop.contains("sdd workflow run agentic-sdd-loop"));
    assert!(codex_loop.contains("docs/AGENTIC-SDD-LOOP.md"));

    let claude_loop =
        fs::read_to_string(root.join(".claude/commands/agentic-sdd-loop.md")).unwrap();
    assert!(claude_loop.contains("sdd workflow run agentic-sdd-loop"));
    assert!(claude_loop.contains("sdd workflow status agentic-sdd-loop"));
    assert!(claude_loop.contains("sdd auto run --real --unattended"));
    assert!(claude_loop.contains("channel: automation"));

    let devin_config = fs::read_to_string(root.join(".devin/config.json")).unwrap();
    assert!(devin_config.contains("\"read_config_from\""));
    assert!(devin_config.contains("\"cursor\": true"));
    assert!(devin_config.contains("\"claude\": true"));
    let devin_rule = fs::read_to_string(root.join(".devin/rules/sdd.md")).unwrap();
    assert!(devin_rule.contains("Skills/workflows Devin"));
    assert!(devin_rule.contains("Subagents Devin"));
    let devin_agent =
        fs::read_to_string(root.join(".devin/agents/sdd-orchestrator/AGENT.md")).unwrap();
    assert!(devin_agent.contains("allowed-tools:"));
    assert!(devin_agent.contains("sdd init \"<nome-da-orquestracao>\""));
    let devin_skill =
        fs::read_to_string(root.join(".devin/skills/orchestration/SKILL.md")).unwrap();
    assert!(devin_skill.contains("name: orchestration"));
    assert!(devin_skill.contains("docs/<slug-da-orquestracao>/"));
    assert_portable_orchestration_skill_contract(&devin_skill);
    let devin_prd = fs::read_to_string(root.join(".devin/skills/prd/SKILL.md")).unwrap();
    assert!(devin_prd.contains("name: prd"));
    assert!(devin_prd.contains("Rode `sdd prd \"$ARGUMENTS\"`"));
    let devin_workflow =
        fs::read_to_string(root.join(".devin/workflows/orchestration.md")).unwrap();
    assert!(devin_workflow.contains("sdd orchestration \"$ARGUMENTS\""));
    assert!(devin_workflow.contains("sdd init \"<nome-da-orquestracao>\""));
    let devin_sdd_workflow = fs::read_to_string(root.join(".devin/workflows/sdd.md")).unwrap();
    assert!(devin_sdd_workflow.contains("sdd clients doctor"));
    assert!(devin_sdd_workflow.contains("sdd clients sync --targets all --force"));
    let devin_prd_workflow = fs::read_to_string(root.join(".devin/workflows/prd.md")).unwrap();
    assert!(devin_prd_workflow.contains("Rode `sdd prd \"$ARGUMENTS\"`"));

    let cursor_routing = fs::read_to_string(root.join(".cursor/model-routing.yaml")).unwrap();
    assert!(cursor_routing.contains("default:"));
    assert!(cursor_routing.contains("stages:"));

    let cursor_rule = fs::read_to_string(root.join(".cursor/rules/sdd.mdc")).unwrap();
    assert!(cursor_rule.contains("sdd init \"<nome-da-orquestracao>\""));
    assert!(cursor_rule.contains("docs/<slug-da-orquestracao>/"));
    assert!(cursor_rule.contains("Contrato de persistência no Cursor"));
    assert!(cursor_rule.contains("sdd orchestration --name \"<nome-da-orquestracao>\""));
    assert!(cursor_rule.contains("não entregue o artefato final apenas na sessão do chat"));
    assert!(cursor_rule.contains("Formato de saída por etapa"));
    assert!(cursor_rule.contains("`prd`: resumo executivo"));
    assert!(cursor_rule.contains("`Diagramas` com Mermaid/Excalidraw"));
    assert!(cursor_rule.contains("`refinement`: documento único e enxuto"));
    let cursor_command =
        fs::read_to_string(root.join(".cursor/commands/orchestration.md")).unwrap();
    assert!(cursor_command.contains("sdd orchestration \"$ARGUMENTS\""));
    assert!(cursor_command.contains("sdd init \"<nome-da-orquestracao>\""));
    let cursor_sdd = fs::read_to_string(root.join(".cursor/commands/sdd.md")).unwrap();
    assert!(cursor_sdd.contains("sdd clients doctor"));
    assert!(cursor_sdd.contains("sdd clients sync --targets all --force"));
    let cursor_prd = fs::read_to_string(root.join(".cursor/commands/prd.md")).unwrap();
    assert!(cursor_prd.contains("Rode `sdd prd \"$ARGUMENTS\"`"));
    let cursor_agent = fs::read_to_string(root.join(".cursor/agents/sdd-orchestrator.md")).unwrap();
    assert!(cursor_agent.contains("name: sdd-orchestrator"));
    assert!(cursor_agent.contains("Formato de saída por etapa"));
    let cursor_skill =
        fs::read_to_string(root.join(".cursor/skills/orchestration/SKILL.md")).unwrap();
    assert!(cursor_skill.contains("name: orchestration"));
    assert_portable_orchestration_skill_contract(&cursor_skill);
    for prefix in [
        ".agents/skills",
        ".claude/skills",
        ".cursor/skills",
        ".opencode/skills",
        ".devin/skills",
        ".trae/skills",
    ] {
        assert_core_stage_skills(root, prefix);
    }
    assert_stage_command_fallbacks(root);
    for rel in [
        ".claude/commands/diagram.md",
        ".cursor/commands/diagram.md",
        ".devin/workflows/diagram.md",
        ".opencode/commands/diagram.md",
        ".trae/commands/diagram.md",
    ] {
        let content = fs::read_to_string(root.join(rel)).unwrap();
        assert_diagram_command_contract(&content);
    }

    let opencode_prd = fs::read_to_string(root.join(".opencode/commands/prd.md")).unwrap();
    assert!(opencode_prd.contains("agent: sdd-orchestrator"));
    assert!(opencode_prd.contains("SDD_PROVIDER=opencode sdd prd \"$ARGUMENTS\""));
    assert!(opencode_prd.contains("Diagramas Mermaid/Excalidraw"));
    assert!(opencode_prd.contains("sdd init \"<nome-da-orquestracao>\""));

    let opencode_loop =
        fs::read_to_string(root.join(".opencode/commands/agentic-sdd-loop.md")).unwrap();
    assert!(opencode_loop.contains("SDD_PROVIDER=opencode sdd workflow run agentic-sdd-loop"));

    let opencode_agent =
        fs::read_to_string(root.join(".opencode/agents/sdd-orchestrator.md")).unwrap();
    assert!(opencode_agent.contains("Formato de saída por etapa"));
    assert!(opencode_agent.contains("`tasks`: épicos"));
    assert!(opencode_agent.contains("`Prompts Agent` standalone por tarefa"));

    let trae_command = fs::read_to_string(root.join(".trae/commands/orchestration.md")).unwrap();
    assert!(trae_command.contains("command -v sdd"));
    assert!(trae_command.contains("cargo run --bin sdd -- orchestration"));
    assert!(trae_command.contains("sdd init \"<nome-da-orquestracao>\""));
    assert!(trae_command.contains("sdd artifact save"));

    let trae_tasks = fs::read_to_string(root.join(".trae/commands/tasks.md")).unwrap();
    assert!(trae_tasks.contains("Antes de rodar comandos `sdd`, resolva o CLI uma vez"));
    assert!(trae_tasks.contains("cargo run --bin sdd --"));
    assert!(trae_tasks.contains("Rode `sdd tasks \"$ARGUMENTS\"`"));
    assert!(trae_tasks
        .to_lowercase()
        .contains("critérios de aceite em gherkin"));
    assert!(trae_tasks.contains("Prompts Agent standalone por tarefa"));

    let trae_skill = fs::read_to_string(root.join(".trae/skills/orchestration/SKILL.md")).unwrap();
    assert!(trae_skill.contains("command -v sdd"));
    assert!(trae_skill.contains("cargo run --bin sdd -- orchestration"));
    assert!(trae_skill.contains("sdd init \"<nome-da-orquestracao>\""));
    assert!(trae_skill.contains("docs/<slug-da-orquestracao>/"));
    assert!(trae_skill.contains("Formato de saída por etapa"));
    assert!(trae_skill.contains("`techspec`: visão técnica"));
    assert!(trae_skill.contains("`Prompts Agent`"));

    let antigravity_rule = fs::read_to_string(root.join(".agents/rules/sdd.md")).unwrap();
    assert!(antigravity_rule.contains("Formato de saída por etapa"));
    assert!(antigravity_rule.contains("`memory`: estado atual"));

    assert_success(run(
        root,
        &[
            "clients",
            "sync",
            "--targets",
            "cursor,trae,opencode,antigravity,devin",
            "--dry-run",
        ],
    ));

    fs::remove_dir_all(root.join(".cursor")).unwrap();
    fs::remove_dir_all(root.join(".devin")).unwrap();
    assert_success(run(root, &["clients", "sync", "--targets", "cursor,devin"]));
    assert!(root.join(".cursor/model-routing.yaml").is_file());
    assert!(root.join(".cursor/rules/sdd.mdc").is_file());
    assert!(root.join(".cursor/commands/sdd.md").is_file());
    assert!(root.join(".cursor/commands/orchestration.md").is_file());
    assert!(root.join(".cursor/commands/prd.md").is_file());
    assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
    assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
    assert_core_stage_skills(root, ".cursor/skills");
    assert!(root.join(".devin/config.json").is_file());
    assert!(root.join(".devin/rules/sdd.md").is_file());
    assert!(root
        .join(".devin/agents/sdd-orchestrator/AGENT.md")
        .is_file());
    assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
    assert_core_stage_skills(root, ".devin/skills");
    assert!(root.join(".devin/workflows/sdd.md").is_file());
    assert!(root.join(".devin/workflows/orchestration.md").is_file());
    assert!(root.join(".devin/workflows/prd.md").is_file());
    assert_portable_orchestration_skill_contract(
        &fs::read_to_string(root.join(".cursor/skills/orchestration/SKILL.md")).unwrap(),
    );
    assert_portable_orchestration_skill_contract(
        &fs::read_to_string(root.join(".devin/skills/orchestration/SKILL.md")).unwrap(),
    );

    let recommendations = combined(&run(root, &["context", "recommend"]));
    assert!(recommendations.contains("React/Next/Vite"));
    assert!(recommendations.contains(".agents/skills/react-frontend/SKILL.md"));
    assert!(recommendations.contains("Uso: trate cada linha como recomendação"));
    assert!(recommendations.contains("Capability map"));
    assert!(recommendations.contains("Matt Pocock Skills"));
    assert!(recommendations.contains("Superpowers"));
    assert!(recommendations.contains("Taste Skill"));
    assert!(recommendations.contains("Code intelligence opcional"));
    assert!(recommendations.contains(".agents/skills/execution-discipline/SKILL.md"));

    let polyglot = tempdir().unwrap();
    let polyglot_root = polyglot.path();
    fs::write(
        polyglot_root.join("go.mod"),
        "module example.com/api\nrequire github.com/gin-gonic/gin v1.10.0\n",
    )
    .unwrap();
    fs::create_dir_all(polyglot_root.join(".github/workflows")).unwrap();
    fs::write(polyglot_root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
    fs::create_dir_all(polyglot_root.join("api")).unwrap();
    fs::write(polyglot_root.join("api/openapi.yaml"), "openapi: 3.1.0\n").unwrap();
    fs::write(polyglot_root.join(".env.example"), "DATABASE_URL=\n").unwrap();
    let polyglot_recommendations = combined(&run(polyglot_root, &["context", "recommend"]));
    assert!(polyglot_recommendations.contains("Go project conventions"));
    assert!(polyglot_recommendations.contains("CI and release readiness"));
    assert!(polyglot_recommendations.contains("API contract conventions"));
    assert!(polyglot_recommendations.contains("Secrets and runtime config handling"));

    assert_success(run(
        root,
        &[
            "discover",
            "--name",
            "Frontend Discovery",
            "--force",
            "brownfield react app",
        ],
    ));
    let discovery =
        fs::read_to_string(root.join("docs/frontend-discovery/00-project-discovery.md")).unwrap();
    assert!(discovery.contains("Recomendações de skills e regras"));
    assert!(discovery.contains("React/Next/Vite"));

    assert_success(run(
        root,
        &[
            "context",
            "recommend",
            "--name",
            "Frontend Discovery",
            "--write",
        ],
    ));
    let written =
        fs::read_to_string(root.join("docs/frontend-discovery/00-context-recommendations.md"))
            .unwrap();
    assert!(written.contains("- Orquestração: Frontend Discovery"));
    assert!(written.contains(".agents/skills/react-frontend/SKILL.md"));
}

#[test]
fn opencode_session_ergonomics_surfaces_are_generated_and_validated() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    fs::remove_dir_all(root.join(".opencode")).unwrap();
    assert_success(run(
        root,
        &["clients", "sync", "--targets", "opencode", "--force"],
    ));

    for rel in [
        ".opencode/commands/verify-changes.md",
        ".opencode/commands/team-review.md",
        ".opencode/commands/team-execution.md",
        ".opencode/commands/security-audit.md",
        ".opencode/commands/test-design.md",
        ".opencode/commands/fast-lane.md",
        ".opencode/skills/code-review/SKILL.md",
        ".opencode/skills/security-privacy/SKILL.md",
        ".opencode/skills/test-strategy/SKILL.md",
        ".opencode/skills/architecture-deepening/SKILL.md",
        ".opencode/skills/execution-discipline/SKILL.md",
        ".opencode/skills/release-readiness/SKILL.md",
        ".opencode/plugins/sdd-security-scan.ts",
        ".opencode/plugins/sdd-verification.ts",
        ".opencode/plugins/sdd-parallel-guard.ts",
    ] {
        assert!(root.join(rel).is_file(), "{rel} should be generated");
    }

    let verify = fs::read_to_string(root.join(".opencode/commands/verify-changes.md")).unwrap();
    assert!(verify.contains("sdd ci"));
    assert!(verify.contains("sdd eval orchestration"));
    assert!(verify.contains("sdd quality report"));
    assert!(verify.contains("checkpoint humano"));
    assert!(verify.contains("docs/<slug-da-orquestracao>/"));

    let team_review = fs::read_to_string(root.join(".opencode/commands/team-review.md")).unwrap();
    assert!(team_review.contains("code-review"));
    assert!(team_review.contains("security-privacy"));
    assert!(team_review.contains("test-strategy"));
    assert!(team_review.contains("fallback sequencial"));

    let fast_lane = fs::read_to_string(root.join(".opencode/commands/fast-lane.md")).unwrap();
    assert!(fast_lane.contains("sdd risk"));
    assert!(fast_lane.contains("baixo risco"));
    assert!(fast_lane.contains("Não autoaprove"));

    let code_review =
        fs::read_to_string(root.join(".opencode/skills/code-review/SKILL.md")).unwrap();
    assert!(code_review.contains("name: code-review"));
    assert!(code_review.contains("Crítico"));

    let security_plugin =
        fs::read_to_string(root.join(".opencode/plugins/sdd-security-scan.ts")).unwrap();
    assert!(security_plugin.contains("SENSITIVE_FILES"));
    assert!(security_plugin.contains(r"\.env"));
    assert!(security_plugin.contains("id_ed25519"));
    assert!(security_plugin.contains("tool.execute.before"));
    assert!(!security_plugin.contains("sk-"));

    let verification_plugin =
        fs::read_to_string(root.join(".opencode/plugins/sdd-verification.ts")).unwrap();
    assert!(verification_plugin.contains("sdd ci"));
    assert!(verification_plugin.contains("/verify-changes"));
    assert!(verification_plugin.contains("Não executa formatador"));

    assert_success(run(
        root,
        &["clients", "sync", "--targets", "all", "--force"],
    ));
    assert_success(run(root, &["clients", "doctor", "--strict"]));
    assert_success(run(
        root,
        &["capabilities", "doctor", "--targets", "opencode"],
    ));

    let capabilities = combined(&run(root, &["capabilities", "list"]));
    assert!(capabilities.contains("opencode-verify-changes"));
    assert!(capabilities.contains("opencode-fast-lane"));
    assert!(capabilities.contains("opencode-parallel-guard-plugin"));

    fs::remove_file(root.join(".opencode/skills/release-readiness/SKILL.md")).unwrap();
    let missing_skill = run(root, &["capabilities", "doctor", "--targets", "opencode"]);
    assert!(
        !missing_skill.status.success(),
        "expected opencode skill collection gap to fail\n{}",
        combined(&missing_skill)
    );
    let missing_skill_text = combined(&missing_skill);
    assert!(missing_skill_text.contains("opencode-selected-skills"));
    assert!(missing_skill_text.contains(".opencode/skills/release-readiness/SKILL.md"));

    assert_success(run(
        root,
        &["clients", "sync", "--targets", "opencode", "--force"],
    ));
    fs::write(
        root.join(".opencode/plugins/sdd-verification.ts"),
        "/* Hook opt-in para OpenCode */\nexport default async () => ({})\n",
    )
    .unwrap();
    let invalid_plugin = run(root, &["capabilities", "doctor", "--targets", "opencode"]);
    assert!(
        !invalid_plugin.status.success(),
        "expected weak opencode plugin to fail\n{}",
        combined(&invalid_plugin)
    );
    let invalid_plugin_text = combined(&invalid_plugin);
    assert!(invalid_plugin_text.contains("missing OpenCode plugin hook export"));
    assert!(invalid_plugin_text.contains(".opencode/plugins/sdd-verification.ts"));
}

#[test]
fn capabilities_catalog_doctor_sync_and_recommend_cover_portable_surfaces() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    fs::write(
        root.join("package.json"),
        r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
    )
    .unwrap();
    assert_success(run(
        repo_root(),
        &[
            "install",
            root.to_str().unwrap(),
            "--preset",
            "frontend-react",
        ],
    ));
    assert_success(run(
        root,
        &["clients", "sync", "--targets", "all", "--force"],
    ));

    let list = combined(&run(root, &["capabilities", "list"]));
    assert!(list.contains("version: 2"));
    assert!(list.contains("orchestration-skill"));
    assert!(list.contains("providers-doctor"));
    assert!(list.contains("hooks-guardrails"));
    assert!(list.contains("opencode-verify-changes"));
    assert!(list.contains("opencode-selected-skills"));
    assert!(list.contains("opencode-verification-plugin"));

    let json = combined(&run(root, &["capabilities", "list", "--json"]));
    let parsed = serde_json::from_str::<Value>(&json).expect("capabilities list json");
    assert_eq!(parsed.get("version").and_then(Value::as_u64), Some(2));
    assert!(json_contains_text(&parsed, "orchestration-command"));
    assert!(json_contains_text(
        &parsed,
        ".opencode/commands/verify-changes.md"
    ));
    assert!(json_contains_text(
        &parsed,
        ".agents/skills/orchestration/SKILL.md"
    ));

    assert_success(run(root, &["capabilities", "doctor", "--targets", "all"]));
    assert_success(run(root, &["clients", "doctor", "--strict"]));

    let sync = combined(&run(
        root,
        &[
            "capabilities",
            "sync",
            "--targets",
            "cursor,devin",
            "--dry-run",
        ],
    ));
    assert!(sync.contains("SDD capabilities sync"));
    assert!(sync.contains(".cursor/commands/orchestration.md"));
    assert!(sync.contains(".devin/workflows/orchestration.md"));

    assert_success(run(root, &["capabilities", "recommend", "--write"]));
    let recommendations =
        fs::read_to_string(root.join(".sdd/capability-recommendations.md")).unwrap();
    assert!(recommendations.contains("Resource adoption map"));
    assert!(recommendations.contains("Núcleo portável"));
    assert!(recommendations.contains("Codex: `.agents/skills`"));
    assert!(recommendations.contains("React/Next/Vite"));
}

#[test]
fn skills_catalog_doctor_sync_and_recommend_cover_hybrid_skills() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    fs::write(
        root.join("package.json"),
        r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
    )
    .unwrap();
    fs::create_dir_all(root.join(".github/workflows")).unwrap();
    fs::write(root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
    assert_success(run(
        repo_root(),
        &[
            "install",
            root.to_str().unwrap(),
            "--preset",
            "frontend-react",
        ],
    ));

    let list = combined(&run(root, &["skills", "list", "--json"]));
    let parsed = serde_json::from_str::<Value>(&list).expect("skills list json");
    assert_eq!(parsed.get("version").and_then(Value::as_u64), Some(1));
    for id in [
        "diagram-design",
        "react-frontend",
        "test-strategy-local",
        "ci-release",
        "bdd-gherkin",
        "code-review",
        "generic-project",
    ] {
        assert!(json_contains_text(&parsed, id), "missing skill id {id}");
    }

    let skills_doctor_output = run(root, &["skills", "doctor", "--strict"]);
    let skills_doctor = combined(&skills_doctor_output);
    assert_success(skills_doctor_output);
    assert!(skills_doctor.contains("╭─ SDD skills doctor"));
    assert!(skills_doctor.contains("│ ✓ PASS"));
    assert!(skills_doctor.contains("◇ Missing (0)"));
    assert!(skills_doctor.contains("◇ Invalid (0)"));
    assert!(skills_doctor.contains("◇ Checked"));
    assert!(
        skills_doctor.find("◇ Missing").unwrap() < skills_doctor.find("◇ Checked").unwrap(),
        "skills issues should be surfaced before checked inventory\n{skills_doctor}"
    );

    let sync = combined(&run(
        root,
        &[
            "skills",
            "sync",
            "--targets",
            "codex,claude,cursor,devin,opencode,trae,antigravity",
            "--dry-run",
        ],
    ));
    assert!(sync.contains("SDD skills sync"));
    assert!(sync.contains(".agents/skills/react-frontend/SKILL.md"));
    assert!(sync.contains(".claude/skills/react-frontend/SKILL.md"));

    assert_success(run(
        root,
        &["skills", "sync", "--targets", "codex,claude", "--force"],
    ));
    let synced_skill = root.join(".agents/skills/react-frontend/SKILL.md");
    assert!(synced_skill.is_file());
    assert!(root
        .join(".claude/skills/diagram-design/assets/template.html")
        .is_file());
    assert!(root
        .join(".claude/skills/diagram-design/references/style-guide.md")
        .is_file());
    assert_success(run(
        root,
        &[
            "skills",
            "validate",
            synced_skill.to_str().unwrap(),
            "--json",
        ],
    ));

    assert_success(run(root, &["skills", "recommend", "--write"]));
    let recommendations = fs::read_to_string(root.join(".sdd/skill-recommendations.md")).unwrap();
    assert!(recommendations.contains("Skill Recommendations"));
    assert!(recommendations.contains("react-frontend"));

    let context = combined(&run(root, &["context", "recommend"]));
    for id in ["react-frontend", "test-strategy-local", "ci-release"] {
        assert!(json_contains_text(&parsed, id));
    }
    assert!(context.contains(".agents/skills/react-frontend/SKILL.md"));
    assert!(context.contains(".agents/skills/local-test-strategy/SKILL.md"));
    assert!(context.contains(".agents/skills/ci-release/SKILL.md"));
}

#[test]
fn skills_sync_covers_source_plugin_mirrors() {
    let sync = combined(&run(
        repo_root(),
        &["skills", "sync", "--targets", "codex", "--dry-run"],
    ));
    assert!(sync.contains("SDD skills sync"));
    assert!(sync.contains("plugins/orchestration/skills/architecture-deepening/SKILL.md"));
    assert!(sync.contains("plugins/orchestration/skills/diagram-design"));
}

#[test]
fn skills_doctor_failure_uses_tui_summary_before_inventory() {
    let target = tempdir().unwrap();
    let root = target.path();
    fs::create_dir_all(root.join("templates")).unwrap();
    fs::write(
        root.join("templates/skill-catalog.yaml"),
        r#"version: 1
skills:
  - id: tiny-skill
    title: Tiny Skill
    description: Minimal failing fixture
    category: test
    status: available
    local_path: .agents/skills/tiny-skill/SKILL.md
"#,
    )
    .unwrap();

    let output = run(root, &["skills", "doctor"]);
    assert_code(output, 1);
    let text = combined(&run(root, &["skills", "doctor"]));
    assert!(text.contains("╭─ SDD skills doctor"));
    assert!(text.contains("│ ✕ FAIL"));
    assert!(text.contains("◇ Missing (1)"));
    assert!(text.contains("◇ Checked (1)"));
    assert!(
        text.find("◇ Missing").unwrap() < text.find("◇ Checked").unwrap(),
        "missing skills should be surfaced before checked inventory\n{text}"
    );
    assert!(text.contains("◇ tiny-skill"));
    assert!(text.contains("missing .agents/skills/tiny-skill/SKILL.md"));
    assert!(!text.contains("- tiny-skill missing"));
}

#[test]
fn workflows_validate_run_and_persist_dynamic_dsl() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    let list = combined(&run(root, &["workflow", "list", "--json"]));
    let workflows = serde_json::from_str::<Value>(&list).expect("workflow list json");
    for id in [
        "standard-sdd",
        "deep-execution-review",
        "discovery-skill-hardening",
        "bugfix-diagnostic-loop",
    ] {
        let paths = workflows
            .get(id)
            .and_then(Value::as_array)
            .unwrap_or_else(|| panic!("missing workflow {id}"));
        assert_eq!(
            paths.len(),
            1,
            "workflow {id} should not list duplicate paths"
        );
        assert_success(run(root, &["workflow", "validate", id, "--json"]));
    }

    let bad_dir = root.join("bad-workflows");
    fs::create_dir_all(&bad_dir).unwrap();
    let cases = [
        (
            "unknown-action.yaml",
            "version: 1\nid: bad-action\ntitle: Bad Action\nnodes:\n  - id: start\n    action: mystery\n",
            "action desconhecida",
        ),
        (
            "missing-destination.yaml",
            "version: 1\nid: missing-destination\ntitle: Missing Destination\nnodes:\n  - id: start\n    action: command\n    command: echo start\n  - id: stranded\n    action: eval\n  - id: end\n    action: notify\nedges:\n  - from: start\n    to: end\n",
            "sem destino",
        ),
        (
            "loop-without-limit.yaml",
            "version: 1\nid: loop-without-limit\ntitle: Loop Without Limit\nnodes:\n  - id: start\n    action: command\n    command: echo start\n  - id: end\n    action: notify\nedges:\n  - from: start\n    to: end\n  - from: end\n    to: start\n",
            "sem max_iterations",
        ),
        (
            "checkpoint-bypass.yaml",
            "version: 1\nid: checkpoint-bypass\ntitle: Checkpoint Bypass\nnodes:\n  - id: gate\n    action: checkpoint\n    stage: prd\n    command: sdd demand approve DEMO --gate prd\n",
            "autoaprovar",
        ),
    ];
    for (name, body, expected) in cases {
        let path = bad_dir.join(name);
        fs::write(&path, body).unwrap();
        let output = run(root, &["workflow", "validate", path.to_str().unwrap()]);
        assert_code(output, 1);
        assert!(
            combined(&run(
                root,
                &["workflow", "validate", path.to_str().unwrap()]
            ))
            .contains(expected),
            "expected validation output to contain {expected}"
        );
    }

    let standard = combined(&run(
        root,
        &[
            "workflow",
            "run",
            "standard-sdd",
            "--input",
            "Criar busca por tags",
            "--max-iterations",
            "10",
            "--json",
        ],
    ));
    let standard = serde_json::from_str::<Value>(&standard).expect("standard workflow json");
    assert_eq!(
        standard.get("status").and_then(Value::as_str),
        Some("awaiting_checkpoint")
    );
    assert!(json_contains_text(&standard, "prd"));
    assert!(json_contains_text(&standard, "criar-busca-por-tags"));

    let auto_root = tempdir().unwrap();
    assert_success(run(
        repo_root(),
        &[
            "install",
            auto_root.path().to_str().unwrap(),
            "--preset",
            "generic",
        ],
    ));
    assert_success(run(
        auto_root.path(),
        &[
            "demand",
            "enqueue",
            "--id",
            "AUTO-1",
            "--type",
            "story",
            "--title",
            "Criar busca por tags",
        ],
    ));
    let auto = combined(&run(
        auto_root.path(),
        &["auto", "run", "--max-iterations", "10", "--json"],
    ));
    assert!(auto.contains("\"produced\": 2"));
    assert!(auto.contains("\"paused\""));
    assert!(auto.contains("criar-busca-por-tags"));

    let deep = combined(&run(
        root,
        &[
            "workflow",
            "run",
            "deep-execution-review",
            "--input",
            "Corrigir regressão",
            "--max-iterations",
            "2",
            "--json",
        ],
    ));
    let deep = serde_json::from_str::<Value>(&deep).expect("deep workflow json");
    assert_eq!(
        deep.get("status").and_then(Value::as_str),
        Some("completed")
    );
    assert_eq!(deep.get("iterations").and_then(Value::as_u64), Some(0));
    assert!(json_contains_text(&deep, "execution"));
    assert!(json_contains_text(&deep, "eval"));
    assert!(json_contains_text(&deep, "review"));
    assert!(root
        .join("docs/corrigir-regressao/06-execution.md")
        .is_file());
    assert!(root.join("docs/corrigir-regressao/07-review.md").is_file());
    assert!(fs::read_to_string(root.join(".sdd/evaluations.jsonl"))
        .unwrap()
        .contains("eval_stage"));
    assert!(root
        .join(".sdd/workflows/deep-execution-review.json")
        .is_file());
    let status = combined(&run(
        root,
        &["workflow", "status", "deep-execution-review", "--json"],
    ));
    let status = serde_json::from_str::<Value>(&status).expect("workflow status json");
    assert_eq!(status.get("status").and_then(Value::as_str), Some("pass"));
    assert!(json_contains_text(&status, "completed"));
}

#[test]
fn capabilities_doctor_rejects_semantic_drift_missing_sources_and_ascii_only_human_content() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));

    let cursor_command = root.join(".cursor/commands/orchestration.md");
    let cursor_text = fs::read_to_string(&cursor_command).unwrap();
    fs::write(
        &cursor_command,
        cursor_text.replace("sdd orchestration", "sdd orchestrator"),
    )
    .unwrap();
    let drift = run(root, &["capabilities", "doctor", "--targets", "cursor"]);
    assert_code(drift, 1);
    let drift_text = combined(&run(
        root,
        &["capabilities", "doctor", "--targets", "cursor"],
    ));
    assert!(drift_text.contains("canonical `sdd orchestration`") || drift_text.contains("drifted"));

    assert_success(run(
        repo_root(),
        &[
            "update",
            "--root",
            root.to_str().unwrap(),
            "--preset",
            "generic",
            "--dry-run",
        ],
    ));
    fs::write(
        root.join(".agents/skills/orchestration/SKILL.md"),
        "---\nname: orchestration\ndescription: Portable orchestration.\n---\nRun `sdd orchestration \"$ARGUMENTS\"`. Treat orchestrator as legacy alias. Use checkpoints and `sdd init \"<name>\"`.\n",
    )
    .unwrap();
    let ascii = run(root, &["capabilities", "doctor", "--targets", "codex"]);
    assert_code(ascii, 1);
    let ascii_text = combined(&run(
        root,
        &["capabilities", "doctor", "--targets", "codex"],
    ));
    assert!(ascii_text.contains("PT-BR UTF-8 accents"));

    let catalog_path = root.join(".sdd/templates/capability-catalog.yaml");
    let mut catalog = fs::read_to_string(&catalog_path).unwrap();
    catalog.push_str(
        "\n  - id: broken-source\n    category: command\n    title: Fonte ausente\n    stages: [review]\n    trigger: Testar fonte ausente.\n    canonical_source: missing/canonical.md\n    adapters:\n      cli:\n        command: sdd missing\n        kind: cli\n    fallback: Registrar falha.\n    validation: [adapter-command]\n    docs: []\n",
    );
    fs::write(&catalog_path, catalog).unwrap();
    let missing = run(root, &["capabilities", "doctor", "--targets", "cli"]);
    assert_code(missing, 1);
    let missing_text = combined(&run(root, &["capabilities", "doctor", "--targets", "cli"]));
    assert!(missing_text.contains("missing canonical_source missing/canonical.md"));
}

#[test]
fn orchestration_plugin_skill_mirrors_include_execution_and_adr_contract() {
    let root = repo_root();
    let canonical = canonical_orchestration_skill();
    let plugin =
        fs::read_to_string(root.join("plugins/orchestration/skills/orchestration/SKILL.md"))
            .unwrap();
    let agents = fs::read_to_string(
        root.join(".agents/skills/orchestration-plugin/skills/orchestration/SKILL.md"),
    )
    .unwrap();
    let claude = fs::read_to_string(
        root.join(".claude/skills/orchestration-plugin/skills/orchestration/SKILL.md"),
    )
    .unwrap();

    assert_eq!(plugin, agents);
    assert_eq!(plugin, claude);

    for (canonical_anchor, plugin_anchor) in [
        (
            "6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff.",
            "6. Execution: uma task por vez, depois de ler o Context Pack de execution e o handoff de code intelligence.",
        ),
        (
            "6a. ADR: create or update Architecture Decision Records from execution evidence.",
            "6a. ADR: crie ou atualize Architecture Decision Records a partir das evidências de execução.",
        ),
        (
            "After Execution, run `sdd adr` when a technical or architectural decision was made or changed.",
            "Após Execution, rode `sdd adr` quando uma decisão técnica ou arquitetural tiver sido criada, confirmada ou alterada.",
        ),
        (
            "Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`",
            "Antes de Execution, gere ou leia `sdd context build --name \"<orquestracao>\" --stage execution --write`",
        ),
        (
            "During `/execution`, use `execution-discipline`",
            "Durante Execution, use `execution-discipline`",
        ),
    ] {
        assert!(
            canonical.contains(canonical_anchor),
            "canonical orchestration skill is missing anchor: {canonical_anchor}"
        );
        assert!(
            plugin.contains(plugin_anchor),
            "plugin orchestration skill is missing localized anchor: {plugin_anchor}"
        );
    }
}

#[test]
fn project_discovery_plugin_skill_mirrors_source_contract() {
    let root = repo_root();
    let plugin =
        fs::read_to_string(root.join("plugins/orchestration/skills/project-discovery/SKILL.md"))
            .unwrap();
    let agents = fs::read_to_string(
        root.join(".agents/skills/orchestration-plugin/skills/project-discovery/SKILL.md"),
    )
    .unwrap();
    let claude = fs::read_to_string(
        root.join(".claude/skills/orchestration-plugin/skills/project-discovery/SKILL.md"),
    )
    .unwrap();

    assert_eq!(plugin, agents);
    assert_eq!(plugin, claude);
    assert!(plugin.contains("sdd discover --json --dry-run"));
    assert!(plugin.contains("sdd context recommend"));
    assert!(plugin.contains("Mapeie linguagem de domínio"));
    assert!(plugin.contains("Mapeie code intelligence opcional"));
    assert!(plugin.contains("Inclua capability map"));
}

#[test]
fn orchestration_plugin_skill_directories_are_complete_mirrors() {
    let root = repo_root();
    let source = root.join("plugins/orchestration/skills");
    assert_directory_mirror(
        &source,
        &root.join(".agents/skills/orchestration-plugin/skills"),
    );
    assert_directory_mirror(
        &source,
        &root.join(".claude/skills/orchestration-plugin/skills"),
    );
}

#[test]
fn default_client_hooks_do_not_block_automatic_loops() {
    let root = repo_root();
    for rel in [
        ".claude/settings.json",
        ".claude/skills/orchestration-plugin/hooks/hooks.json",
        ".agents/skills/orchestration-plugin/hooks/hooks.json",
    ] {
        let text = fs::read_to_string(root.join(rel)).unwrap();
        let value = serde_json::from_str::<Value>(&text).unwrap();
        let hooks = value
            .get("hooks")
            .and_then(Value::as_object)
            .unwrap_or_else(|| panic!("{rel} missing hooks object"));
        for event in [
            "PreToolUse",
            "PostToolUse",
            "SubagentStop",
            "TaskCreated",
            "TaskCompleted",
            "TeammateIdle",
            "Notification",
        ] {
            assert_eq!(
                hooks.get(event).and_then(Value::as_array).map(Vec::len),
                Some(0),
                "{rel} must keep {event} empty by default"
            );
        }
    }
}

#[test]
fn opencode_prompt_file_refs_are_repaired_and_validated() {
    let target = tempdir().unwrap();
    let root = target.path();
    fs::write(
        root.join("opencode.json"),
        r##"{
  "$schema": "https://opencode.ai/config.json",
  "agent": {
    "frontend-dev": {
      "description": "Frontend development specialist",
      "mode": "subagent",
      "prompt": "{file:.opencode/agents/frontend-dev.md}",
      "color": "#3b82f6"
    },
    "api-dev": {
      "description": "API specialist",
      "mode": "subagent",
      "prompt": "{$file:.opencode/agents/api-dev.md}",
      "color": "#f59e0b"
    }
  }
}"##,
    )
    .unwrap();

    assert_success(run(
        repo_root(),
        &["install", root.to_str().unwrap(), "--preset", "generic"],
    ));
    let frontend_prompt = root.join(".opencode/agents/frontend-dev.md");
    let api_prompt = root.join(".opencode/agents/api-dev.md");
    assert!(frontend_prompt.is_file());
    assert!(api_prompt.is_file());
    let frontend_content = fs::read_to_string(&frontend_prompt).unwrap();
    assert!(frontend_content.contains("opencode.json"));
    assert!(frontend_content.contains("AGENTS.md"));
    assert_success(run(root, &["clients", "doctor"]));

    fs::remove_file(&frontend_prompt).unwrap();
    let doctor = run(root, &["clients", "doctor"]);
    assert_code(doctor, 1);
    let doctor_text = combined(&run(root, &["clients", "doctor"]));
    assert!(doctor_text.contains("╭─ SDD clients doctor"));
    assert!(doctor_text.contains("│ ✕ FAIL"));
    assert!(doctor_text.contains("Invalid"));
    assert!(doctor_text.contains("◇ Covered"));
    assert!(
        doctor_text.find("◇ Invalid").unwrap() < doctor_text.find("◇ Covered").unwrap(),
        "invalid client issues should be surfaced before covered inventory\n{doctor_text}"
    );
    assert!(doctor_text.contains(".opencode/agents/frontend-dev.md"));
    assert!(!doctor_text.contains("- opencode:"));

    assert_success(run(root, &["clients", "sync", "--targets", "opencode"]));
    assert!(frontend_prompt.is_file());
    assert_success(run(root, &["clients", "doctor"]));

    let init_target = tempdir().unwrap();
    let init_root = init_target.path();
    fs::write(
        init_root.join("opencode.json"),
        r#"{"agent":{"frontend-dev":{"prompt":"{file:.opencode/agents/frontend-dev.md}"}}}"#,
    )
    .unwrap();
    assert_success(run(init_root, &["init", "--preset", "generic"]));
    assert!(init_root.join(".opencode/agents/frontend-dev.md").is_file());
    assert_success(run(init_root, &["clients", "doctor"]));

    fs::write(
        root.join("opencode.json"),
        r#"{"agent":{"docs":{"prompt":"{file:.opencode/prompts/docs.md}"}}}"#,
    )
    .unwrap();
    let unrepaired = run(root, &["clients", "doctor"]);
    assert_code(unrepaired, 1);
    assert!(combined(&run(root, &["clients", "doctor"])).contains(".opencode/prompts/docs.md"));
}

#[test]
fn validators_risk_and_hooks_cover_old_script_behavior() {
    let root = repo_root();
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "checkpoint",
            "tests/fixtures/good/checkpoint.md",
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "project-discovery",
            "tests/fixtures/good/project-discovery.md",
        ],
    ));
    assert_code(
        run(
            root,
            &[
                "validate-artifact",
                "project-discovery",
                "tests/fixtures/bad/project-discovery-missing-capability-map.md",
            ],
        ),
        1,
    );
    assert_code(
        run(
            root,
            &[
                "validate-artifact",
                "prd",
                "tests/fixtures/bad/prd-missing-sections.md",
            ],
        ),
        1,
    );
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "prd",
            "tests/fixtures/good/prd-diagrams.md",
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "prd",
            "tests/fixtures/good/prd-diagrams-subheading.md",
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "prd",
            "tests/fixtures/good/prd-visual-companion.md",
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "techspec",
            "tests/fixtures/good/techspec-visual-companion.md",
        ],
    ));
    let invalid_prd = run(
        root,
        &[
            "validate-artifact",
            "prd",
            "tests/fixtures/bad/prd-invalid-diagrams.md",
        ],
    );
    assert_code(invalid_prd, 1);
    assert!(combined(&run(
        root,
        &[
            "validate-artifact",
            "prd",
            "tests/fixtures/bad/prd-invalid-diagrams.md",
        ],
    ))
    .contains("Diagramas"));
    assert_code(
        run(
            root,
            &[
                "validate-artifact",
                "prd",
                "tests/fixtures/bad/prd-visual-companion-outside-assets.md",
            ],
        ),
        1,
    );
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "tasks",
            "tests/fixtures/good/tasks-prompts-agent.md",
        ],
    ));
    assert_success(run(
        root,
        &[
            "validate-artifact",
            "tasks",
            "tests/fixtures/good/tasks-prompts-agent-subheading.md",
        ],
    ));
    assert_code(
        run(
            root,
            &[
                "validate-artifact",
                "tasks",
                "tests/fixtures/bad/tasks-prompts-agent-missing-task.md",
            ],
        ),
        1,
    );
    assert_code(
        run(
            root,
            &[
                "validate-artifact",
                "checkpoint",
                "tests/fixtures/bad/checkpoint-missing-decision-action.md",
            ],
        ),
        1,
    );

    let risk = run(root, &["risk", "login SSO com permissao e dados pessoais"]);
    assert_success(risk);
    let risk_text = combined(&run(
        root,
        &["risk", "login SSO com permissao e dados pessoais"],
    ));
    assert!(risk_text.contains("risk_level: high"));
    assert!(risk_text.contains("data_sensitive"));

    let hook_project = tempdir().unwrap();
    let bad_task = r#"{"hook_event_name":"TaskCreated","task":{"description":"fazer tela"}}"#;
    assert_code(
        run_with(
            root,
            &["hook", "task-gate"],
            Some(bad_task),
            &[("CLAUDE_PROJECT_DIR", hook_project.path())],
        ),
        2,
    );
    let good_task = r#"{"hook_event_name":"TaskCreated","task":{"description":"Origem PRD-1. Dado um usuario, Quando salva, Entao persiste. Definition of Done com testes."}}"#;
    assert_success(run_with(
        root,
        &["hook", "task-gate"],
        Some(good_task),
        &[("CLAUDE_PROJECT_DIR", hook_project.path())],
    ));

    assert_code(
        run_with(
            root,
            &["hook", "bash-guard"],
            Some(r#"{"tool_input":{"command":"git reset --hard HEAD"}}"#),
            &[],
        ),
        2,
    );
    assert_success(run_with(
        root,
        &["hook", "bash-guard"],
        Some(r#"{"tool_input":{"command":"git push origin feature-x"}}"#),
        &[],
    ));
    assert_code(
        run_with(
            root,
            &["hook", "bash-guard"],
            Some(r#"{"tool_input":{"command":"git push --force-with-lease origin feature-x"}}"#),
            &[],
        ),
        2,
    );
    assert_success(run_with_env_values(
        root,
        &["hook", "bash-guard"],
        Some(r#"{"tool_input":{"command":"git push --force-with-lease origin feature-x"}}"#),
        &[("SDD_GUARD_APPROVED", "1")],
    ));
    assert_code(
        run_with(
            root,
            &["hook", "write-guard"],
            Some(r#"{"tool_name":"Write","agent_type":"prd"}"#),
            &[],
        ),
        2,
    );

    fs::create_dir_all(hook_project.path().join("schemas")).unwrap();
    fs::copy(
        root.join("schemas/artifact-sections.json"),
        hook_project.path().join("schemas/artifact-sections.json"),
    )
    .unwrap();
    // Nome de etapa canônico (`02-prd.md`): o gate do hook só atua sobre artefatos
    // do artifact store, então usa-se o nome determinístico que a pipeline grava.
    let bad_prd = hook_project.path().join("02-prd.md");
    fs::write(&bad_prd, "# PRD\n\n## Resumo\nIncompleto.\n").unwrap();
    // No Windows o caminho tem `\`, que é escape inválido em JSON — normaliza para
    // `/` (aceito pelo filesystem do Windows) para montar um payload JSON válido.
    let bad_prd_json = bad_prd.display().to_string().replace('\\', "/");
    let payload = format!(
        r#"{{"hook_event_name":"PostToolUse","tool_name":"Write","tool_input":{{"file_path":"{}"}}}}"#,
        bad_prd_json
    );
    assert_code(
        run_with(
            root,
            &["hook", "artifact-gate"],
            Some(&payload),
            &[("CLAUDE_PROJECT_DIR", hook_project.path())],
        ),
        2,
    );

    // Regressão: arquivos que não são artefatos SDD (sem nome de etapa canônico)
    // nunca são gateados, mesmo com conteúdo que casaria a heurística por pista.
    let changelog = hook_project.path().join("CHANGELOG.md");
    fs::write(
        &changelog,
        "# Changelog\n\n## 0.0.1\n\n- Feature: discovery e tasks de exemplo.\n",
    )
    .unwrap();
    let changelog_json = changelog.display().to_string().replace('\\', "/");
    let changelog_payload = format!(
        r#"{{"hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{{"file_path":"{}"}}}}"#,
        changelog_json
    );
    assert_success(run_with(
        root,
        &["hook", "artifact-gate"],
        Some(&changelog_payload),
        &[("CLAUDE_PROJECT_DIR", hook_project.path())],
    ));

    assert_success(run_with(
        root,
        &["hook", "trace-log"],
        Some(
            r#"{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"cargo test"}}"#,
        ),
        &[("CLAUDE_PROJECT_DIR", hook_project.path())],
    ));
    assert_success(run_with(
        root,
        &["hook", "notify"],
        Some(r#"{"hook_event_name":"Notification","matcher":"approval"}"#),
        &[("CLAUDE_PROJECT_DIR", hook_project.path())],
    ));
    assert_success(run_with(
        root,
        &["hook", "subagent-audit"],
        Some(
            r#"{"hook_event_name":"SubagentStop","agent_type":"review","transcript_path":"/tmp/transcript"}"#,
        ),
        &[("CLAUDE_PROJECT_DIR", hook_project.path())],
    ));
    assert!(hook_project.path().join(".sdd/events.jsonl").is_file());
    assert!(hook_project
        .path()
        .join(".sdd/notifications.jsonl")
        .is_file());
    assert!(hook_project.path().join(".sdd/subagents.jsonl").is_file());
}

#[test]
fn diagram_attach_copies_visual_asset_and_updates_stage_artifact() {
    let tmp = tempdir().unwrap();
    let root = tmp.path();
    assert_success(run(root, &["init", "Fluxo Visual"]));
    assert_success(run(
        root,
        &[
            "idea",
            "--name",
            "Fluxo Visual",
            "--force",
            "mapear jornada visual",
        ],
    ));
    let idea = root.join("docs/fluxo-visual/01-idea.md");
    let initial = fs::read_to_string(&idea).unwrap();
    assert!(initial.contains("## Diagramas"));

    let source = root.join("visual.html");
    fs::write(&source, "<!doctype html><svg></svg>\n").unwrap();
    assert_success(run(
        root,
        &[
            "diagram",
            "attach",
            "--name",
            "Fluxo Visual",
            "--stage",
            "idea",
            "--file",
            "visual.html",
            "--title",
            "Jornada principal",
            "--source",
            "01-idea.md",
        ],
    ));

    let updated = fs::read_to_string(&idea).unwrap();
    assert!(updated.contains("<!-- sdd-diagram:jornada-principal -->"));
    assert!(updated.contains("assets/diagrams/idea-jornada-principal.html"));
    assert!(root
        .join("docs/fluxo-visual/assets/diagrams/idea-jornada-principal.html")
        .is_file());
    let installed_skill = root.join(".agents/skills/diagram-design");
    fs::create_dir_all(installed_skill.join("references")).unwrap();
    fs::write(
        installed_skill.join("SKILL.md"),
        "---\nname: diagram-design\ndescription: test bundle\n---\n",
    )
    .unwrap();
    fs::write(
        installed_skill.join("references/style-guide.md"),
        "paper: #f5f5f5\nink: #2d3142\naccent: #eb6c36\nmuted: #4f5d75\n",
    )
    .unwrap();
    let default_doctor = combined(&run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]));
    assert!(default_doctor.contains("Style guide: default"));

    fs::write(
        installed_skill.join("references/style-guide.md"),
        "accent: #125c9c\n",
    )
    .unwrap();
    let custom_doctor = run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]);
    assert_success(custom_doctor);
    let custom_doctor = combined(&run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]));
    assert!(custom_doctor.contains("Style guide: custom"));
}
