use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, Stdio};

use directories::BaseDirs;
use gtc::config::GtcConfig;
use gtc::error::{GtcError, GtcResult};

use super::deploy::{ChildProcessEnv, StartTarget};
use super::toolchain::{
    InstalledReleaseArtifact, installed_release_artifacts, installed_toolchain_label,
};
use super::{
    BUNDLE_BIN, COMPONENT_BIN, DEPLOYER_BIN, DEV_BIN, DW_BIN, FLOW_BIN, OP_BIN, PACK_BIN,
    RUNNER_BIN, SECRETS_BIN, SETUP_BIN, START_BIN,
};
use crate::i18n_support::{t, t_or};

pub(super) fn run_binary_checked(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    operation: &str,
) -> GtcResult<()> {
    run_binary_checked_with_target_and_env(binary, args, debug, locale, operation, None, None)
}

pub(super) fn run_binary_checked_with_target(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    operation: &str,
    target: Option<StartTarget>,
) -> GtcResult<()> {
    run_binary_checked_with_target_and_env(binary, args, debug, locale, operation, target, None)
}

pub(super) fn run_binary_checked_with_target_and_env(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    operation: &str,
    target: Option<StartTarget>,
    extra_env: Option<&ChildProcessEnv>,
) -> GtcResult<()> {
    let status =
        run_binary_status_with_target_and_env(binary, args, debug, locale, target, extra_env)?;
    if status.success() {
        return Ok(());
    }
    Err(GtcError::message(format!(
        "{operation} failed via {binary} with status {}",
        status.code().unwrap_or(1)
    )))
}

pub(super) fn run_binary_capture(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
) -> GtcResult<String> {
    run_binary_capture_with_target(binary, args, debug, locale, None)
}

pub(super) fn run_binary_capture_with_target(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    target: Option<StartTarget>,
) -> GtcResult<String> {
    if debug {
        eprintln!("{} {} {:?}", t(locale, "gtc.debug.exec"), binary, args);
    }
    let command = resolve_binary_command(binary);
    let mut process = ProcessCommand::new(&command);
    process.args(args).env("GREENTIC_LOCALE", locale);
    apply_default_deploy_env_for_target(&mut process, target, locale)?;
    let output = process
        .output()
        .map_err(|err| GtcError::io(format!("failed to execute {binary}"), err))?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        if stderr.is_empty() {
            return Err(GtcError::message(format!(
                "{binary} exited with status {}",
                output.status.code().unwrap_or(1)
            )));
        }
        return Err(GtcError::message(stderr));
    }
    String::from_utf8(output.stdout)
        .map_err(|err| GtcError::message(format!("invalid UTF-8 from {binary}: {err}")))
}

pub(super) fn run_binary_status_with_target_and_env(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    target: Option<StartTarget>,
    extra_env: Option<&ChildProcessEnv>,
) -> GtcResult<std::process::ExitStatus> {
    if debug {
        eprintln!("{} {} {:?}", t(locale, "gtc.debug.exec"), binary, args);
    }
    let command = resolve_binary_command(binary);
    let mut process = ProcessCommand::new(&command);
    process
        .args(args)
        .env("GREENTIC_LOCALE", locale)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());
    apply_default_deploy_env_for_target(&mut process, target, locale)?;
    if let Some(extra_env) = extra_env {
        extra_env.apply(&mut process);
    }
    process
        .status()
        .map_err(|err| GtcError::io(format!("failed to execute {binary}"), err))
}

pub(super) fn apply_default_deploy_env_for_target(
    _process: &mut ProcessCommand,
    _target: Option<StartTarget>,
    _locale: &str,
) -> GtcResult<()> {
    Ok(())
}

pub(super) fn resolve_cargo_bin_dir() -> GtcResult<PathBuf> {
    if let Some(cargo_home) = GtcConfig::from_env().cargo_home() {
        return Ok(cargo_home.join("bin"));
    }

    let base =
        BaseDirs::new().ok_or_else(|| GtcError::message("failed to resolve home directory"))?;
    Ok(base.home_dir().join(".cargo").join("bin"))
}

pub(super) fn passthrough(binary: &str, args: &[String], debug: bool, locale: &str) -> i32 {
    passthrough_in_dir_with_env(binary, args, debug, locale, None, None)
}

pub(super) fn passthrough_with_env(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    extra_env: &ChildProcessEnv,
) -> i32 {
    passthrough_in_dir_with_env(binary, args, debug, locale, None, Some(extra_env))
}

pub(super) fn passthrough_in_dir(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    cwd: Option<&Path>,
) -> i32 {
    passthrough_in_dir_with_env(binary, args, debug, locale, cwd, None)
}

fn passthrough_in_dir_with_env(
    binary: &str,
    args: &[String],
    debug: bool,
    locale: &str,
    cwd: Option<&Path>,
    extra_env: Option<&ChildProcessEnv>,
) -> i32 {
    if debug {
        eprintln!("{} {} {:?}", t(locale, "gtc.debug.exec"), binary, args);
    }

    let command = resolve_binary_command(binary);

    let mut process = ProcessCommand::new(&command);
    process
        .args(args)
        .env("GREENTIC_LOCALE", locale)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());
    if let Some(cwd) = cwd {
        process.current_dir(cwd);
    }
    if let Some(extra_env) = extra_env {
        extra_env.apply(&mut process);
    }

    match process.status() {
        Ok(status) => status.code().unwrap_or(1),
        Err(err) => {
            if err.kind() == std::io::ErrorKind::NotFound {
                match binary {
                    DEV_BIN => print_missing_dev_message(locale),
                    OP_BIN => print_missing_op_message(locale),
                    SETUP_BIN => eprintln!("{}", t(locale, "gtc.err.bin_missing_setup")),
                    _ => eprintln!("{}", t(locale, "gtc.err.exec_failed")),
                }
            } else {
                eprintln!("{}: {err}", t(locale, "gtc.err.exec_failed"));
            }
            1
        }
    }
}

pub(super) fn run_doctor(locale: &str) -> i32 {
    let mut failed = false;

    println!(
        "Greentic toolchain release: {}",
        installed_toolchain_label()
    );

    for binary in [
        DEV_BIN,
        OP_BIN,
        BUNDLE_BIN,
        COMPONENT_BIN,
        FLOW_BIN,
        PACK_BIN,
        RUNNER_BIN,
        SECRETS_BIN,
        SETUP_BIN,
        DEPLOYER_BIN,
        START_BIN,
    ] {
        let command = resolve_binary_command(binary);
        match ProcessCommand::new(&command).arg("--version").output() {
            Ok(output) => {
                let status_label = if output.status.success() {
                    t(locale, "gtc.doctor.ok")
                } else {
                    t(locale, "gtc.doctor.warn")
                };
                let version = first_non_empty_line(&String::from_utf8_lossy(&output.stdout))
                    .or_else(|| first_non_empty_line(&String::from_utf8_lossy(&output.stderr)))
                    .unwrap_or_else(|| t(locale, "gtc.doctor.version_unavailable").into_owned());
                println!("{binary}: {status_label} ({version}) [{}]", command);
            }
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                failed = true;
                println!(
                    "{binary}: {} [{}]",
                    t(locale, "gtc.doctor.missing"),
                    command
                );
                match binary {
                    DEV_BIN => print_missing_dev_message(locale),
                    OP_BIN => print_missing_op_message(locale),
                    BUNDLE_BIN => eprintln!(
                        "{}",
                        t_or(
                            locale,
                            "gtc.err.bin_missing_bundle",
                            "greentic-bundle not found in PATH. Install with: cargo install greentic-bundle",
                        )
                    ),
                    SETUP_BIN => eprintln!("{}", t(locale, "gtc.err.bin_missing_setup")),
                    DEPLOYER_BIN => eprintln!(
                        "{}",
                        t_or(
                            locale,
                            "gtc.err.bin_missing_deployer",
                            "greentic-deployer not found in PATH. Install with: cargo install greentic-deployer",
                        )
                    ),
                    _ => {}
                }
            }
            Err(err) => {
                failed = true;
                println!(
                    "{binary}: {} ({err}) [{}]",
                    t(locale, "gtc.doctor.missing"),
                    command
                );
            }
        }
    }

    if let Err(err) = print_installed_release_artifacts() {
        failed = true;
        println!(
            "Greentic release artifacts: {} ({err})",
            t(locale, "gtc.doctor.warn")
        );
    }

    print_knowledge_memory_readiness(locale);

    if failed { 1 } else { 0 }
}

/// Prints the "Knowledge & Memory readiness" section of `gtc doctor`.
///
/// These checks are informational only: absence of any of these env vars is
/// perfectly fine for workers that don't use memory or knowledge (RAG), so a
/// WARN here never flips the overall doctor exit code.
fn print_knowledge_memory_readiness(locale: &str) {
    println!();
    println!(
        "{}",
        t_or(
            locale,
            "gtc.doctor.knowledge_memory.header",
            "Knowledge & Memory readiness (for workers that use memory or knowledge, you also need):",
        )
    );

    let env = EnvView::from_process_env();
    for line in knowledge_memory_readiness(&env) {
        let status_label = match line.status {
            ReadinessStatus::Ok => t(locale, "gtc.doctor.ok"),
            ReadinessStatus::Warn => t(locale, "gtc.doctor.warn"),
        };
        println!("  {}: {status_label} ({})", line.label, line.detail);
    }
}

/// Readiness status for a single [`ReadinessLine`].
///
/// Deliberately has no "error"/"fail" variant: knowledge/memory capabilities
/// are optional, so the worst outcome a missing env var can produce here is a
/// WARN, never a doctor-exit-code failure.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ReadinessStatus {
    Ok,
    Warn,
}

/// One reported line of the knowledge/memory readiness section.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ReadinessLine {
    pub(super) label: &'static str,
    pub(super) status: ReadinessStatus,
    pub(super) detail: String,
}

/// A minimal, injectable view over environment variables.
///
/// Keeps [`knowledge_memory_readiness`] pure and unit-testable: production
/// code builds an [`EnvView`] from the real process environment once, and
/// tests build one from an in-memory map without mutating `std::env`.
pub(super) struct EnvView {
    vars: HashMap<String, String>,
}

impl EnvView {
    pub(super) fn from_process_env() -> Self {
        Self {
            vars: env::vars().collect(),
        }
    }

    #[cfg(test)]
    pub(super) fn from_pairs<I, K, V>(pairs: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        Self {
            vars: pairs
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
        }
    }

    fn is_set(&self, key: &str) -> bool {
        self.vars.get(key).is_some_and(|value| !value.is_empty())
    }

    fn has_prefix(&self, prefix: &str) -> bool {
        self.vars
            .iter()
            .any(|(key, value)| key.starts_with(prefix) && !value.is_empty())
    }
}

/// Pure readiness logic for the "Knowledge & Memory readiness" doctor section.
///
/// Reports on three independent runtime capabilities that only work on a
/// knowledge-enabled runner build (`runner-full`, built with the
/// `knowledge-chronicle` / `long-term-chronicle` cargo features):
/// short-term agent state, RAG embedding config, and long-term (Chronicle)
/// memory. None of these are required for workers that don't use memory or
/// knowledge, so every line here is OK/WARN, never a hard failure.
pub(super) fn knowledge_memory_readiness(env: &EnvView) -> Vec<ReadinessLine> {
    vec![
        short_term_memory_readiness(env),
        knowledge_embedding_readiness(env),
        long_term_memory_readiness(env),
    ]
}

const AW_REDIS_URL_KEY: &str = "GREENTIC_AW_REDIS_URL";

fn short_term_memory_readiness(env: &EnvView) -> ReadinessLine {
    if env.is_set(AW_REDIS_URL_KEY) {
        ReadinessLine {
            label: "Short-term memory / agent state",
            status: ReadinessStatus::Ok,
            detail: format!(
                "{AW_REDIS_URL_KEY} is set; dw.agent short-term state can persist on a server."
            ),
        }
    } else {
        ReadinessLine {
            label: "Short-term memory / agent state",
            status: ReadinessStatus::Warn,
            detail: format!(
                "{AW_REDIS_URL_KEY} is not set. Workers using dw.agent short-term state need \
                 it on a server; for local runs, the desktop-agent-ephemeral no-infra \
                 alternative works without it."
            ),
        }
    }
}

const KNOWLEDGE_EMBED_KEYS: [&str; 3] = [
    "GREENTIC_KNOWLEDGE_EMBED_BASE_URL",
    "GREENTIC_KNOWLEDGE_EMBED_API_KEY",
    "GREENTIC_KNOWLEDGE_EMBED_MODEL",
];

fn knowledge_embedding_readiness(env: &EnvView) -> ReadinessLine {
    let missing: Vec<&str> = KNOWLEDGE_EMBED_KEYS
        .into_iter()
        .filter(|key| !env.is_set(key))
        .collect();

    if missing.is_empty() {
        ReadinessLine {
            label: "Knowledge / RAG (embedding)",
            status: ReadinessStatus::Ok,
            detail: "embedding configuration is complete.".to_string(),
        }
    } else {
        ReadinessLine {
            label: "Knowledge / RAG (embedding)",
            status: ReadinessStatus::Warn,
            detail: format!(
                "missing {}. Knowledge retrieval will be inactive until these are set and the \
                 runner is built with the knowledge-chronicle feature (runner-full).",
                missing.join(", ")
            ),
        }
    }
}

const CHRONICLE_PREFIX: &str = "GREENTIC_CHRONICLE_";

fn long_term_memory_readiness(env: &EnvView) -> ReadinessLine {
    if env.has_prefix(CHRONICLE_PREFIX) {
        ReadinessLine {
            label: "Long-term memory (Chronicle)",
            status: ReadinessStatus::Ok,
            detail: format!("{CHRONICLE_PREFIX}* configuration detected."),
        }
    } else {
        ReadinessLine {
            label: "Long-term memory (Chronicle)",
            status: ReadinessStatus::Warn,
            detail: format!(
                "no {CHRONICLE_PREFIX}* variables set. Long-term memory needs a \
                 long-term-chronicle-enabled runner build (runner-full) plus this environment."
            ),
        }
    }
}

fn print_installed_release_artifacts() -> GtcResult<()> {
    let Some(artifacts) = installed_release_artifacts()? else {
        println!("Greentic release artifacts: not installed");
        return Ok(());
    };

    println!(
        "Greentic release artifacts: {} ({}) [{}]",
        artifacts.release,
        artifacts.channel,
        artifacts.index_path.display()
    );
    print_release_artifact_group(&format!("{} packs", artifacts.channel), &artifacts.packs);
    print_release_artifact_group(
        &format!("{} components", artifacts.channel),
        &artifacts.components,
    );
    Ok(())
}

fn print_release_artifact_group(label: &str, artifacts: &[InstalledReleaseArtifact]) {
    if artifacts.is_empty() {
        println!("{label}: none cached");
        return;
    }
    println!("{label}:");
    for artifact in artifacts {
        println!(
            "  {} -> {} ({}) [{}]",
            artifact.reference, artifact.canonical_ref, artifact.version, artifact.digest
        );
    }
}

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

fn print_missing_dev_message(locale: &str) {
    eprintln!("{}", t(locale, "gtc.err.bin_missing_dev"));
    eprintln!(
        "{}",
        t_or(
            locale,
            "gtc.err.bin_missing_dev_install_hint",
            "Run `gtc install` first."
        )
    );
}

fn print_missing_op_message(locale: &str) {
    eprintln!("{}", t(locale, "gtc.err.bin_missing_op"));
    eprintln!(
        "{}",
        t_or(
            locale,
            "gtc.err.bin_missing_op_install_hint",
            "Run `gtc install` first."
        )
    );
}

fn resolve_binary_command(binary: &str) -> String {
    let invocation = env::args().next();
    if let Some(path) = resolve_companion_binary_from_parts(
        invocation.as_deref(),
        env::current_exe().ok().as_deref(),
        binary,
    ) {
        return path.display().to_string();
    }
    let physical_binary = companion_binary_for_invocation(invocation.as_deref(), binary);
    match binary {
        DEV_BIN => GtcConfig::from_env()
            .dev_bin_override()
            .map(|value| value.to_string_lossy().to_string())
            .unwrap_or(physical_binary),
        _ => physical_binary,
    }
}

pub(super) fn resolve_companion_binary(binary: &str) -> Option<PathBuf> {
    let invocation = env::args().next();
    resolve_companion_binary_from_parts(
        invocation.as_deref(),
        env::current_exe().ok().as_deref(),
        binary,
    )
}

pub(super) fn resolve_companion_command(binary: &str) -> PathBuf {
    resolve_companion_binary(binary).unwrap_or_else(|| {
        let invocation = env::args().next();
        PathBuf::from(companion_binary_for_invocation(
            invocation.as_deref(),
            binary,
        ))
    })
}

#[cfg(test)]
pub(super) fn resolve_companion_binary_from(
    current_exe: Option<&Path>,
    binary: &str,
) -> Option<PathBuf> {
    resolve_companion_binary_from_parts(None, current_exe, binary)
}

#[cfg(test)]
pub(super) fn resolve_companion_binary_from_invocation(
    invocation: Option<&str>,
    current_exe: Option<&Path>,
    binary: &str,
) -> Option<PathBuf> {
    resolve_companion_binary_from_parts(invocation, current_exe, binary)
}

fn resolve_companion_binary_from_parts(
    invocation: Option<&str>,
    current_exe: Option<&Path>,
    binary: &str,
) -> Option<PathBuf> {
    let env_override = companion_binary_env_override(binary);
    if let Some(path) = env_override {
        return Some(PathBuf::from(path));
    }

    let physical_binary = companion_binary_for_invocation(invocation, binary);
    if let Some(current_exe) = current_exe {
        let exe_dir = current_exe.parent()?;
        if let Some(sibling) = resolve_binary_in_dir(exe_dir, &physical_binary) {
            return Some(sibling);
        }

        if let Some(workspace_candidate) =
            resolve_workspace_local_binary(current_exe, binary, &physical_binary)
        {
            return Some(workspace_candidate);
        }
    }

    if let Ok(cargo_bin_dir) = resolve_cargo_bin_dir()
        && let Some(cargo_candidate) = resolve_binary_in_dir(&cargo_bin_dir, &physical_binary)
    {
        return Some(cargo_candidate);
    }
    None
}

fn companion_binary_for_invocation(invocation: Option<&str>, binary: &str) -> String {
    if !invocation_has_dev_suffix(invocation) || !is_greentic_companion_binary(binary) {
        return binary.to_string();
    }
    format!("{binary}-dev")
}

fn invocation_has_dev_suffix(invocation: Option<&str>) -> bool {
    invocation
        .and_then(|value| Path::new(value).file_stem())
        .and_then(|stem| stem.to_str())
        .is_some_and(|stem| stem.ends_with("-dev"))
}

fn is_greentic_companion_binary(binary: &str) -> bool {
    matches!(
        binary,
        DEV_BIN
            | OP_BIN
            | BUNDLE_BIN
            | COMPONENT_BIN
            | DEPLOYER_BIN
            | FLOW_BIN
            | PACK_BIN
            | RUNNER_BIN
            | SECRETS_BIN
            | SETUP_BIN
            | START_BIN
            | DW_BIN
    )
}

pub(super) fn resolve_binary_in_dir(dir: &Path, binary: &str) -> Option<PathBuf> {
    binary_file_candidates(dir, binary)
        .into_iter()
        .find(|candidate| candidate.is_file())
}

fn binary_file_candidates(dir: &Path, binary: &str) -> Vec<PathBuf> {
    let mut candidates = vec![dir.join(binary)];
    let exe_suffix = env::consts::EXE_SUFFIX;
    if !exe_suffix.is_empty() && !binary.ends_with(exe_suffix) {
        candidates.push(dir.join(format!("{binary}{exe_suffix}")));
    }
    candidates
}

fn resolve_workspace_local_binary(
    current_exe: &Path,
    logical_binary: &str,
    physical_binary: &str,
) -> Option<PathBuf> {
    let repo_dir = current_exe.parent()?.parent()?.parent()?;
    let workspace_root = repo_dir.parent()?;
    let repo_name = match logical_binary {
        DEV_BIN => "greentic-dev",
        OP_BIN => "greentic-operator",
        BUNDLE_BIN => "greentic-bundle",
        COMPONENT_BIN => "greentic-component",
        DEPLOYER_BIN => "greentic-deployer",
        FLOW_BIN => "greentic-flow",
        PACK_BIN => "greentic-pack",
        RUNNER_BIN => "greentic-runner",
        SECRETS_BIN => "greentic-secrets",
        SETUP_BIN => "greentic-setup",
        START_BIN => "greentic-start",
        _ => return None,
    };
    let candidate = workspace_root
        .join(repo_name)
        .join("target")
        .join("debug")
        .as_path()
        .to_path_buf();
    resolve_binary_in_dir(&candidate, physical_binary)
}

fn companion_binary_env_override(binary: &str) -> Option<std::ffi::OsString> {
    let cfg = GtcConfig::from_env();
    match binary {
        DEV_BIN => cfg.dev_bin_override(),
        OP_BIN => cfg.operator_bin_override(),
        BUNDLE_BIN => cfg.bundle_bin_override(),
        COMPONENT_BIN => cfg.component_bin_override(),
        DEPLOYER_BIN => cfg.deployer_bin_override(),
        FLOW_BIN => cfg.flow_bin_override(),
        PACK_BIN => cfg.pack_bin_override(),
        RUNNER_BIN => cfg.runner_bin_override(),
        SECRETS_BIN => cfg.secrets_bin_override(),
        SETUP_BIN => cfg.setup_bin_override(),
        START_BIN => cfg.start_bin_override(),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    #[cfg(unix)]
    use super::{apply_default_deploy_env_for_target, run_binary_capture, run_binary_checked};
    use super::{first_non_empty_line, is_greentic_companion_binary, resolve_cargo_bin_dir};
    #[cfg(unix)]
    use crate::deploy::StartTarget;
    use crate::tests::env_test_lock;
    #[cfg(unix)]
    use crate::tests::fake_deployer_contract;
    use std::env;
    #[cfg(unix)]
    use std::process::Command as ProcessCommand;

    #[test]
    fn first_non_empty_line_skips_blank_lines() {
        assert_eq!(
            first_non_empty_line("\n  \nhello\nworld"),
            Some("hello".to_string())
        );
        assert_eq!(first_non_empty_line(" \n\t"), None);
    }

    #[test]
    fn resolve_cargo_bin_dir_prefers_env_override() {
        let _guard = env_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let dir = tempfile::tempdir().expect("tempdir");
        unsafe {
            env::set_var("CARGO_HOME", dir.path());
        }

        let resolved = resolve_cargo_bin_dir().expect("cargo bin");

        unsafe {
            env::remove_var("CARGO_HOME");
        }
        assert_eq!(resolved, dir.path().join("bin"));
    }

    #[test]
    #[cfg(unix)]
    fn apply_default_deploy_env_for_target_leaves_deployer_defaults_unset() {
        let _guard = env_test_lock().lock().unwrap_or_else(|e| e.into_inner());
        let _deployer = fake_deployer_contract(None);
        unsafe {
            env::remove_var("GREENTIC_DEPLOY_TERRAFORM_VAR_OPERATOR_IMAGE");
            env::remove_var("GREENTIC_DEPLOY_TERRAFORM_VAR_OPERATOR_IMAGE_DIGEST");
        }

        let mut cmd = ProcessCommand::new("env");
        apply_default_deploy_env_for_target(&mut cmd, Some(StartTarget::Aws), "en")
            .expect("default deploy env");
        let envs: Vec<_> = cmd.get_envs().collect();

        assert!(!envs.iter().any(|(key, value)| {
            key.to_string_lossy() == "GREENTIC_DEPLOY_TERRAFORM_VAR_OPERATOR_IMAGE"
                && value.is_some()
        }));
        assert!(!envs.iter().any(|(key, value)| {
            key.to_string_lossy() == "GREENTIC_DEPLOY_TERRAFORM_VAR_OPERATOR_IMAGE_DIGEST"
                && value.is_some()
        }));
    }

    #[cfg(unix)]
    #[test]
    fn run_binary_capture_returns_stdout() {
        let output = run_binary_capture(
            "/bin/sh",
            &["-c".to_string(), "printf ok".to_string()],
            false,
            "en",
        )
        .expect("capture");
        assert_eq!(output, "ok");
    }

    #[cfg(unix)]
    #[test]
    fn run_binary_checked_reports_non_zero_exit() {
        let err = run_binary_checked(
            "/bin/sh",
            &["-c".to_string(), "exit 7".to_string()],
            false,
            "en",
            "demo operation",
        )
        .unwrap_err();
        assert!(
            err.to_string()
                .contains("demo operation failed via /bin/sh with status 7")
        );
    }

    #[test]
    fn is_greentic_companion_binary_recognizes_greentic_dw() {
        assert!(is_greentic_companion_binary("greentic-dw"));
    }

    use super::{EnvView, ReadinessStatus, knowledge_memory_readiness};

    #[test]
    fn knowledge_memory_readiness_all_set_reports_ok() {
        let env = EnvView::from_pairs([
            ("GREENTIC_AW_REDIS_URL", "redis://localhost:6379"),
            (
                "GREENTIC_KNOWLEDGE_EMBED_BASE_URL",
                "https://embed.example.com",
            ),
            ("GREENTIC_KNOWLEDGE_EMBED_API_KEY", "secret"),
            ("GREENTIC_KNOWLEDGE_EMBED_MODEL", "text-embedding-3-small"),
            ("GREENTIC_CHRONICLE_URL", "https://chronicle.example.com"),
        ]);

        let lines = knowledge_memory_readiness(&env);

        assert_eq!(lines.len(), 3);
        assert!(
            lines.iter().all(|line| line.status == ReadinessStatus::Ok),
            "expected all lines OK, got: {lines:?}"
        );
    }

    #[test]
    fn knowledge_memory_readiness_missing_embedding_vars_warns_with_names_and_runner_full() {
        let env =
            EnvView::from_pairs([("GREENTIC_KNOWLEDGE_EMBED_MODEL", "text-embedding-3-small")]);

        let lines = knowledge_memory_readiness(&env);

        let embedding_line = lines
            .iter()
            .find(|line| line.label == "Knowledge / RAG (embedding)")
            .expect("embedding readiness line present");

        assert_eq!(embedding_line.status, ReadinessStatus::Warn);
        assert!(
            embedding_line
                .detail
                .contains("GREENTIC_KNOWLEDGE_EMBED_BASE_URL"),
            "detail should name the missing base-url var: {}",
            embedding_line.detail
        );
        assert!(
            embedding_line
                .detail
                .contains("GREENTIC_KNOWLEDGE_EMBED_API_KEY"),
            "detail should name the missing api-key var: {}",
            embedding_line.detail
        );
        assert!(
            !embedding_line
                .detail
                .contains("GREENTIC_KNOWLEDGE_EMBED_MODEL"),
            "detail should not name a var that is set: {}",
            embedding_line.detail
        );
        assert!(
            embedding_line.detail.contains("runner-full"),
            "detail should mention the runner-full build requirement: {}",
            embedding_line.detail
        );
    }

    #[test]
    fn knowledge_memory_readiness_missing_redis_warns_with_ephemeral_alternative() {
        let empty_pairs: [(&str, &str); 0] = [];
        let env = EnvView::from_pairs(empty_pairs);

        let lines = knowledge_memory_readiness(&env);

        let short_term_line = lines
            .iter()
            .find(|line| line.label == "Short-term memory / agent state")
            .expect("short-term readiness line present");

        assert_eq!(short_term_line.status, ReadinessStatus::Warn);
        assert!(
            short_term_line.detail.contains("GREENTIC_AW_REDIS_URL"),
            "detail should name the missing var: {}",
            short_term_line.detail
        );
        assert!(
            short_term_line.detail.contains("desktop-agent-ephemeral"),
            "detail should mention the no-infra alternative: {}",
            short_term_line.detail
        );
    }

    #[test]
    fn knowledge_memory_readiness_missing_chronicle_vars_warns_with_long_term_chronicle_build() {
        let env = EnvView::from_pairs([
            ("GREENTIC_AW_REDIS_URL", "redis://localhost:6379"),
            (
                "GREENTIC_KNOWLEDGE_EMBED_BASE_URL",
                "https://embed.example.com",
            ),
            ("GREENTIC_KNOWLEDGE_EMBED_API_KEY", "secret"),
            ("GREENTIC_KNOWLEDGE_EMBED_MODEL", "text-embedding-3-small"),
        ]);

        let lines = knowledge_memory_readiness(&env);

        let chronicle_line = lines
            .iter()
            .find(|line| line.label == "Long-term memory (Chronicle)")
            .expect("chronicle readiness line present");

        assert_eq!(chronicle_line.status, ReadinessStatus::Warn);
        assert!(
            chronicle_line.detail.contains("long-term-chronicle"),
            "detail should mention the long-term-chronicle build requirement: {}",
            chronicle_line.detail
        );
    }
}
