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")
        .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")
        .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"));
}

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 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();
    assert!(json_contains_text(&installed_mcp, root.to_str().unwrap()));
    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.to_str().unwrap()));
    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.to_str().unwrap()));
    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 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("\"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("opencode-go/deepseek-v4-flash"));
    assert!(providers.contains("opencode-go/deepseek-v4-pro"));
    assert!(providers.contains("opencode-go/kimi-k2.6"));
    assert!(providers.contains("opencode-go/qwen3.7-plus"));

    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("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 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"));
}

#[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_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 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_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 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 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);

    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("Rode `sdd prd \"$ARGUMENTS\"`"));
    assert!(opencode_prd.contains("Diagramas Mermaid/Excalidraw"));
    assert!(opencode_prd.contains("sdd init \"<nome-da-orquestracao>\""));

    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!(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/skills/prd/SKILL.md").is_file());
    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 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",
        ],
    ));

    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"));

    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,
        ".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 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 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("Invalid"));
    assert!(doctor_text.contains(".opencode/agents/frontend-dev.md"));

    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",
        ],
    ));
    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_success(run(
        root,
        &[
            "validate-artifact",
            "tasks",
            "tests/fixtures/good/tasks-prompts-agent.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_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());
}
