//! System prompt template source and constants.
//!
//! Templates are XOR-obfuscated by `scripts/encrypt_templates.py` (obfuscation,
//! not security — seeds live in-repo) so they don't appear as obvious plaintext
//! in `strings` output. They are decrypted on demand and the returned
//! `Zeroizing<String>` wipes the plaintext from memory on drop.

use zeroize::Zeroizing;

// Encrypted template bytes (pre-generated by scripts/encrypt_templates.py).
#[path = "prompt_encrypted.rs"]
mod prompt_encrypted;
use prompt_encrypted::*;

/// Decrypt XOR-obfuscated template data (mirrors `scripts/encrypt_templates.py::xor_encrypt`).
/// Obfuscation only — not a security boundary.
fn decrypt(data: &[u8], seed: u8) -> Zeroizing<String> {
    let bytes: Vec<u8> = data
        .iter()
        .enumerate()
        .map(|(i, &b)| b ^ seed.wrapping_add(i as u8))
        .collect();
    Zeroizing::new(String::from_utf8(bytes).expect(
        "prompt template decryption produced invalid UTF-8 — \
         prompt_encrypted.rs is likely stale; run: \
         python3 scripts/encrypt_templates.py",
    ))
}

/// The base prompt template (decrypted fresh; zeroed on drop).
pub(crate) fn base_template() -> Zeroizing<String> {
    decrypt(BASE_PROMPT_ENC, PROMPT_SEEDS[0])
}

/// The base prompt template source, exposed for `kigi prompt --section template`.
pub fn base_template_source() -> Zeroizing<String> {
    base_template()
}

pub(crate) fn apply_patch_template() -> Zeroizing<String> {
    decrypt(CODEX_PROMPT_ENC, PROMPT_SEEDS[1])
}

/// Apply-patch prompt template source, exposed for `kigi prompt --section apply-patch-template`.
pub fn apply_patch_template_source() -> Zeroizing<String> {
    apply_patch_template()
}

/// The subagent-specific base template (decrypted fresh; zeroed on drop).
pub(crate) fn subagent_template() -> Zeroizing<String> {
    decrypt(SUBAGENT_PROMPT_ENC, PROMPT_SEEDS[2])
}

/// The compact system prompt used after conversation compaction.
pub const COMPACT_SYSTEM_PROMPT: &str = "You are an AI coding agent. You operate in a workspace with a provided codebase.\n\n\
     Your main goal is to complete the user's request, denoted within the <user_query> tag.";

#[cfg(test)]
mod tests {
    use super::*;
    use kigi_tools::types::template_renderer::TemplateRenderer;
    use kigi_tools::types::tool::ToolKind;
    use std::collections::HashMap;

    /// Verify the pre-generated encrypted file matches the current template sources.
    /// If this fails, run: `python3 scripts/encrypt_templates.py`
    #[test]
    fn test_encrypted_templates_not_stale() {
        fn xor_encrypt(data: &[u8], seed: u8) -> Vec<u8> {
            data.iter()
                .enumerate()
                .map(|(i, &b)| b ^ seed.wrapping_add(i as u8))
                .collect()
        }
        let base_raw = include_bytes!("../../templates/prompt.md");
        let apply_patch_raw = include_bytes!("../../templates/apply_patch_prompt.md");
        let subagent_raw = include_bytes!("../../templates/subagent_prompt.md");

        assert_eq!(
            BASE_PROMPT_ENC,
            &xor_encrypt(base_raw, PROMPT_SEEDS[0]),
            "prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py"
        );
        assert_eq!(
            CODEX_PROMPT_ENC,
            &xor_encrypt(apply_patch_raw, PROMPT_SEEDS[1]),
            "apply_patch_prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py"
        );
        assert_eq!(
            SUBAGENT_PROMPT_ENC,
            &xor_encrypt(subagent_raw, PROMPT_SEEDS[2]),
            "subagent_prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py"
        );
    }

    /// Build a TemplateRenderer with the standard kigi tool kinds.
    fn default_renderer() -> TemplateRenderer {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "read_file"),
            (ToolKind::Edit, "search_replace"),
            (ToolKind::Execute, "run_terminal_command"),
            (ToolKind::Search, "grep"),
            (ToolKind::List, "list_dir"),
            (ToolKind::Plan, "todo_write"),
            (ToolKind::Skill, "skill"),
            (
                ToolKind::BackgroundTaskAction,
                "get_command_or_subagent_output",
            ),
            (ToolKind::KillTaskAction, "kill_command_or_subagent"),
            (ToolKind::WebSearch, "web_search"),
        ]
        .into_iter()
        .map(|(k, v)| (k, v.to_string()))
        .collect();
        TemplateRenderer::new(tools, HashMap::new())
    }

    fn default_placeholders() -> serde_json::Value {
        serde_json::json!({
            "os_name": "macos",
            "shell_path": "/bin/zsh",
            "working_directory": "/tmp/test",
            "current_date": "2025-01-15",
            "memory_enabled": false,
            "is_non_interactive": false,
            "system_prompt_label": crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL,
        })
    }

    fn render_base(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String {
        let tmpl = base_template();
        renderer
            .render_with_extra(&tmpl, placeholders)
            .expect("base template render failed")
    }

    fn render_subagent(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String {
        let tmpl = subagent_template();
        renderer
            .render_with_extra(&tmpl, placeholders)
            .expect("subagent template render failed")
    }

    fn render_apply_patch(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String {
        let tmpl = apply_patch_template();
        renderer
            .render_with_extra(&tmpl, placeholders)
            .expect("codex template render failed")
    }

    // ── Variable substitution ───────────────────────────────────────

    #[test]
    fn test_variable_substitution_tool_kind() {
        let r = default_renderer();
        let p = default_placeholders();
        let result = r
            .render_with_extra("Use ${{ tools.by_kind.read }} to read files.", &p)
            .unwrap();
        assert_eq!(result, "Use read_file to read files.");
    }

    #[test]
    fn test_variable_substitution_agent_fields() {
        let r = default_renderer();
        let p = default_placeholders();
        let result = r
            .render_with_extra("OS: ${{ os_name }}, Shell: ${{ shell_path }}", &p)
            .unwrap();
        assert_eq!(result, "OS: macos, Shell: /bin/zsh");
    }

    // ── Conditionals ────────────────────────────────────────────────

    #[test]
    fn test_conditional_tool_present() {
        let r = default_renderer();
        let p = default_placeholders();
        let result = r
            .render_with_extra("${%- if tools.by_kind.plan %}show${%- endif %}", &p)
            .unwrap();
        assert_eq!(result, "show");
    }

    #[test]
    fn test_conditional_tool_absent() {
        // Renderer without plan tool
        let tools: HashMap<ToolKind, String> = [(ToolKind::Read, "read_file".to_string())].into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let p = default_placeholders();
        let result = r
            .render_with_extra("${%- if tools.by_kind.plan %}show${%- endif %}", &p)
            .unwrap();
        assert_eq!(result, "");
    }

    #[test]
    fn test_literal_braces_pass_through() {
        let r = default_renderer();
        let p = default_placeholders();
        let result = r
            .render_with_extra("Use {{ literal_braces }} in prose.", &p)
            .unwrap();
        assert_eq!(result, "Use {{ literal_braces }} in prose.");
    }

    // ── Tool name overrides ─────────────────────────────────────────

    #[test]
    fn test_tool_name_override() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "view_file".to_string()),
            (ToolKind::Edit, "Edit".to_string()),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let p = default_placeholders();
        let result = r
            .render_with_extra(
                "Use ${{ tools.by_kind.read }} and ${{ tools.by_kind.edit }}.",
                &p,
            )
            .unwrap();
        assert_eq!(result, "Use view_file and Edit.");
    }

    // ── Base template rendering ─────────────────────────────────────

    #[test]
    fn test_base_template_renders() {
        let prompt = render_base(&default_renderer(), &default_placeholders());
        assert!(prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL));
        assert!(prompt.contains("user_query"));
    }

    #[test]
    fn test_base_template_contains_resolved_tool_names() {
        let prompt = render_base(&default_renderer(), &default_placeholders());
        // The minimal prompt only resolves the read/edit tool names, inside
        // <tool_calling>. (todo_write / run_terminal_command lived in sections
        // that the trimmed prompt no longer renders.)
        assert!(prompt.contains("read_file"), "Should contain 'read_file'");
        assert!(
            prompt.contains("search_replace"),
            "Should contain 'search_replace'"
        );
        assert!(!prompt.contains("${{"), "No unresolved template variables");
        assert!(!prompt.contains("${%"), "No unresolved template blocks");
    }

    #[test]
    fn test_base_template_with_overridden_tool_names() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "view_file".to_string()),
            (ToolKind::Edit, "edit".to_string()),
            (ToolKind::Execute, "run_terminal_cmd".to_string()),
            (ToolKind::Search, "grep".to_string()),
            (ToolKind::Plan, "todo_write".to_string()),
            (
                ToolKind::BackgroundTaskAction,
                "get_task_output".to_string(),
            ),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            prompt.contains("`view_file`"),
            "Should use overridden 'view_file'"
        );
        assert!(prompt.contains("`edit`"), "Should use overridden 'edit'");
        assert!(
            !prompt.contains("`read_file`"),
            "Should NOT contain canonical 'read_file'"
        );
    }

    #[test]
    fn test_base_template_plan_absent_omits_task_management() {
        // Renderer without Plan tool
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "read_file".to_string()),
            (ToolKind::Execute, "run_terminal_cmd".to_string()),
            (
                ToolKind::BackgroundTaskAction,
                "get_task_output".to_string(),
            ),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            !prompt.contains("Task Management"),
            "Task Management section should be omitted"
        );
    }

    #[test]
    fn test_base_template_execute_absent_omits_background_tasks() {
        // Renderer without Execute tool
        let tools: HashMap<ToolKind, String> = [(ToolKind::Plan, "todo_write".to_string())].into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            !prompt.contains("background_tasks"),
            "background_tasks section should be omitted"
        );
    }

    #[test]
    fn test_monitor_tool_renders_watch_section() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Execute, "run_command".to_string()),
            (ToolKind::BackgroundTaskAction, "get_output".to_string()),
            (ToolKind::KillTaskAction, "kill_task".to_string()),
            (ToolKind::Monitor, "monitor".to_string()),
        ]
        .into_iter()
        .collect();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            prompt.contains("For watch processes"),
            "monitor section should render when Monitor tool is present"
        );
        assert!(
            prompt.contains("streams each stdout line back as a chat notification"),
            "monitor section should describe streaming stdout as notifications"
        );
        assert!(
            prompt.contains("Use the `monitor` tool"),
            "monitor section should resolve the Monitor tool name"
        );
    }

    #[test]
    fn test_no_monitor_tool_omits_watch_section() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Execute, "run_command".to_string()),
            (ToolKind::BackgroundTaskAction, "get_output".to_string()),
            (ToolKind::KillTaskAction, "kill_task".to_string()),
        ]
        .into_iter()
        .collect();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            !prompt.contains("For watch processes"),
            "monitor section should NOT render without Monitor tool"
        );
        assert!(
            !prompt.contains("<background_tasks>"),
            "background_tasks section is gated on the Monitor tool and is omitted without it"
        );
    }

    // ── Required sections regression ────────────────────────────────

    #[test]
    fn test_base_template_contains_required_sections() {
        let p = default_placeholders();
        let prompt = render_base(&default_renderer(), &p);
        assert!(
            prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL),
            "Must contain agent identity"
        );
        assert!(
            prompt.contains("user_query"),
            "Must reference user_query tag"
        );
    }

    #[test]
    fn test_compact_prompt_matches_expected() {
        assert_eq!(
            COMPACT_SYSTEM_PROMPT,
            "You are an AI coding agent. You operate in a workspace with a provided codebase.\n\n\
             Your main goal is to complete the user's request, denoted within the <user_query> tag.",
        );
    }

    // ── Mid-session mode switching ──────────────────────────────────

    #[test]
    fn test_mid_session_switch_concise_to_full() {
        let compact = COMPACT_SYSTEM_PROMPT;
        assert!(!compact.contains("read_file"), "Compact has no tool names");
        assert!(
            !compact.contains("<tool_calling>"),
            "Compact has no tool section"
        );

        let full = render_base(&default_renderer(), &default_placeholders());
        assert!(
            full.contains("<tool_calling>"),
            "Full prompt has tool section"
        );
        assert!(full.contains("read_file"), "Full prompt has read_file");
        assert!(
            full.contains("search_replace"),
            "Full prompt has search_replace"
        );
    }

    #[test]
    fn test_mid_session_switch_preserves_tool_overrides() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "view".to_string()),
            (ToolKind::Edit, "edit".to_string()),
            (ToolKind::Execute, "run_terminal_cmd".to_string()),
            (ToolKind::Plan, "todo_write".to_string()),
            (
                ToolKind::BackgroundTaskAction,
                "get_task_output".to_string(),
            ),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(prompt.contains("`edit`"), "Should use overridden 'edit'");
        assert!(prompt.contains("`view`"), "Should use overridden 'view'");
        assert!(
            !prompt.contains("`read_file`"),
            "Should not contain original 'read_file'"
        );
        assert!(
            !prompt.contains("`search_replace`"),
            "Should not contain original 'search_replace'"
        );
    }

    // ── Determinism ─────────────────────────────────────────────────

    #[test]
    fn test_prompt_deterministic_across_renders() {
        let r = default_renderer();
        let p = default_placeholders();
        let a = render_base(&r, &p);
        let b = render_base(&r, &p);
        assert_eq!(a, b, "Prompt rendering must be deterministic");
    }

    #[test]
    fn test_full_mode_deterministic() {
        let r = default_renderer();
        let p = default_placeholders();
        let body = "Agent: ${{ tools.by_kind.read }}, OS: ${{ os_name }}";
        let a = r.render_with_extra(body, &p).unwrap();
        let b = r.render_with_extra(body, &p).unwrap();
        assert_eq!(a, b, "Full mode rendering must be deterministic");
    }

    // ── Disabled tools ──────────────────────────────────────────────

    #[test]
    fn test_disabled_tools_omit_sections() {
        // No plan, no execute
        let tools: HashMap<ToolKind, String> = [(ToolKind::Read, "read_file".to_string())].into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_base(&r, &default_placeholders());
        assert!(
            !prompt.contains("Task Management"),
            "Task Management must be omitted"
        );
        assert!(
            !prompt.contains("background_tasks"),
            "background_tasks must be omitted"
        );
    }

    // ── Memory section ──────────────────────────────────────────────

    #[test]
    fn test_memory_enabled_does_not_render_memory_section() {
        // The <memory> section was removed from the minimal base prompt.
        // Even when the memory tools are registered AND memory_enabled=true,
        // the trimmed template must not render a memory section. (Complements
        // test_memory_disabled_omits_memory_section, which covers the default.)
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "read_file".to_string()),
            (ToolKind::MemorySearch, "memory_search".to_string()),
            (ToolKind::MemoryGet, "memory_get".to_string()),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let mut p = default_placeholders();
        p["memory_enabled"] = serde_json::json!(true);
        let prompt = render_base(&r, &p);
        assert!(
            !prompt.contains("<memory>"),
            "Memory section was removed from the minimal prompt"
        );
        assert!(
            !prompt.contains("### Memory Management"),
            "Memory Management section was removed from the minimal prompt"
        );
        assert!(
            !prompt.contains("memory_search"),
            "memory tool names must not appear once the memory section is gone"
        );
        assert!(
            !prompt.contains("memory_get"),
            "memory tool names must not appear once the memory section is gone"
        );
    }

    #[test]
    fn test_memory_disabled_omits_memory_section() {
        let prompt = render_base(&default_renderer(), &default_placeholders());
        assert!(
            !prompt.contains("<memory>"),
            "Memory section must be omitted"
        );
    }

    // ── Web search disabled ─────────────────────────────────────────

    #[test]
    fn test_web_search_disabled_renders_without_crash() {
        // No Fetch tool
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "read_file".to_string()),
            (ToolKind::Plan, "todo_write".to_string()),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let tmpl = base_template();
        let result = r.render_with_extra(&tmpl, &default_placeholders());
        assert!(
            result.is_ok(),
            "Must render without crash: {:?}",
            result.err()
        );
    }

    // ── Apply-patch template rendering ───────────────────────────────────

    #[test]
    fn test_apply_patch_template_renders() {
        let prompt = render_apply_patch(&default_renderer(), &default_placeholders());
        assert!(prompt.contains("coding agent"));
    }

    #[test]
    fn test_apply_patch_template_contains_resolved_tool_names() {
        let prompt = render_apply_patch(&default_renderer(), &default_placeholders());
        assert!(prompt.contains("todo_write"), "Should contain 'todo_write'");
        // apply_patch is hardcoded, not resolved via ${{ tools.by_kind.edit }}
        assert!(
            prompt.contains("apply_patch"),
            "Should contain hardcoded 'apply_patch'"
        );
        assert!(!prompt.contains("${{"), "No unresolved template variables");
        assert!(!prompt.contains("${%"), "No unresolved template blocks");
    }

    #[test]
    fn test_apply_patch_template_plan_absent_omits_planning() {
        // Renderer without Plan tool
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "read_file".to_string()),
            (ToolKind::Edit, "search_replace".to_string()),
            (ToolKind::Execute, "run_terminal_cmd".to_string()),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_apply_patch(&r, &default_placeholders());
        assert!(
            !prompt.contains("## Planning"),
            "Planning section should be omitted when plan tool absent"
        );
        assert!(
            !prompt.contains("update_plan"),
            "update_plan references should be omitted"
        );
    }

    #[test]
    fn test_apply_patch_template_plan_present_includes_planning() {
        let prompt = render_apply_patch(&default_renderer(), &default_placeholders());
        assert!(
            prompt.contains("## Planning"),
            "Planning section should be present when plan tool exists"
        );
    }

    #[test]
    fn test_apply_patch_template_with_overridden_tool_names() {
        let tools: HashMap<ToolKind, String> = [
            (ToolKind::Read, "view_file".to_string()),
            (ToolKind::Edit, "some_other_edit".to_string()),
            (ToolKind::Execute, "run_terminal_cmd".to_string()),
            (ToolKind::Plan, "update_plan".to_string()),
            (
                ToolKind::BackgroundTaskAction,
                "get_task_output".to_string(),
            ),
        ]
        .into();
        let r = TemplateRenderer::new(tools, HashMap::new());
        let prompt = render_apply_patch(&r, &default_placeholders());
        // apply_patch is hardcoded — NOT affected by Edit tool override
        assert!(
            prompt.contains("`apply_patch`"),
            "apply_patch must remain hardcoded regardless of edit override"
        );
        assert!(
            !prompt.contains("some_other_edit"),
            "Edit override must NOT leak into apply-patch prompt"
        );
        // Plan tool IS resolved via template
        assert!(
            prompt.contains("`update_plan`"),
            "Should use overridden 'update_plan'"
        );
    }

    #[test]
    fn test_apply_patch_template_deterministic_across_renders() {
        let r = default_renderer();
        let p = default_placeholders();
        let a = render_apply_patch(&r, &p);
        let b = render_apply_patch(&r, &p);
        assert_eq!(a, b, "Apply-patch template rendering must be deterministic");
    }

    #[test]
    fn test_subagent_template_deterministic_across_renders() {
        let r = default_renderer();
        let p = default_placeholders();
        let a = render_subagent(&r, &p);
        let b = render_subagent(&r, &p);
        assert_eq!(a, b, "Subagent template rendering must be deterministic");
    }

    // ── Task completion discipline ─────────────────────────────────
    //
    // The `<task_completion_discipline>` block was removed from both
    // base and subagent templates. These tests pin the deletion so the
    // block doesn't accidentally come back, and so the runtime TodoGate
    // doesn't start firing reminders that reference a non-existent
    // block.

    #[test]
    fn task_completion_discipline_block_is_not_rendered() {
        let prompt = render_base(&default_renderer(), &default_placeholders());
        assert!(
            !prompt.contains("<task_completion_discipline>"),
            "discipline block was removed from the base template"
        );
        let subagent = render_subagent(&default_renderer(), &default_placeholders());
        assert!(
            !subagent.contains("<task_completion_discipline>"),
            "discipline block was removed from the subagent template"
        );
    }

    /// Soft byte ceiling shared by both prompt-size budget tests.
    /// Forward-budget guard against runaway growth, not a tight target.
    const PROMPT_SIZE_SOFT_CEILING_BYTES: usize = 16384;

    fn assert_template_size_under(prompt: &str, label: &str) {
        assert!(
            prompt.len() < PROMPT_SIZE_SOFT_CEILING_BYTES,
            "{label} prompt is {} bytes, exceeding soft ceiling of {} bytes",
            prompt.len(),
            PROMPT_SIZE_SOFT_CEILING_BYTES,
        );
    }

    #[test]
    fn test_base_template_size_budget() {
        let prompt = render_base(&default_renderer(), &default_placeholders());
        assert_template_size_under(&prompt, "base");
    }

    #[test]
    fn test_subagent_template_size_budget() {
        let prompt = render_subagent(&default_renderer(), &default_placeholders());
        assert_template_size_under(&prompt, "subagent");
    }

    // ── Guard invariant ─────────────────────────────────────────────
    // Every `${{ tools.by_kind.X }}` must sit inside a `${%- if ... %}`
    // whose condition requires X (contains `tools.by_kind.X` at a word
    // boundary, with no top-level ` or `). If violated, X could render
    // as empty string at runtime.

    fn word_bounded(hay: &str, needle: &str) -> bool {
        let mut s = 0;
        while let Some(i) = hay[s..].find(needle) {
            let end = s + i + needle.len();
            match hay[end..].chars().next() {
                None => return true,
                Some(c) if !(c.is_alphanumeric() || c == '_') => return true,
                _ => s += i + 1,
            }
        }
        false
    }

    fn guarantees(cond: &str, kind: &str) -> bool {
        if word_bounded(cond, &format!("tools.by_kind.{kind}")) && !cond.contains(" or ") {
            return true;
        }
        false
    }

    fn assert_guards(template: &str, label: &str) {
        let bytes = template.as_bytes();
        let mut stack: Vec<String> = Vec::new();
        let mut errors: Vec<String> = Vec::new();
        let mut i = 0;
        while i + 2 < bytes.len() {
            let three = &bytes[i..i + 3];
            if three == b"${%" {
                let end = bytes[i + 3..]
                    .windows(2)
                    .position(|w| w == b"%}")
                    .map(|e| i + 3 + e + 2)
                    .unwrap_or(bytes.len());
                let body = std::str::from_utf8(&bytes[i + 3..end - 2])
                    .unwrap()
                    .trim_matches(['-', ' ']);
                if let Some(c) = body.strip_prefix("if ") {
                    stack.push(c.trim().into());
                } else if let Some(c) = body.strip_prefix("elif ") {
                    stack.pop();
                    stack.push(c.trim().into());
                } else if body == "else" {
                    stack.pop();
                    stack.push("<else>".into());
                } else if body == "endif" {
                    stack.pop();
                }
                i = end;
            } else if three == b"${{" {
                let end = bytes[i + 3..]
                    .windows(2)
                    .position(|w| w == b"}}")
                    .map(|e| i + 3 + e + 2)
                    .unwrap_or(bytes.len());
                let body = std::str::from_utf8(&bytes[i + 3..end - 2]).unwrap().trim();
                // search_tool and use_tool are always built-in, so they
                // never need a guard.
                const ALWAYS_BUILTIN: &[&str] = &["search_tool", "use_tool"];
                if let Some(kind) = body.strip_prefix("tools.by_kind.")
                    && kind.chars().all(|c| c.is_alphanumeric() || c == '_')
                    && !ALWAYS_BUILTIN.contains(&kind)
                    && !stack.iter().any(|c| guarantees(c, kind))
                {
                    let line = template[..i].lines().count() + 1;
                    errors.push(format!(
                        "{label}:{line}: unguarded `${{{{ tools.by_kind.{kind} }}}}` (stack: {stack:?})"
                    ));
                }
                i = end;
            } else {
                i += 1;
            }
        }
        assert!(errors.is_empty(), "\n  {}", errors.join("\n  "));
    }

    #[test]
    fn test_template_vars_are_always_guarded() {
        assert_guards(&base_template(), "prompt.md");
        assert_guards(&subagent_template(), "subagent_prompt.md");
        assert_guards(&apply_patch_template(), "apply_patch_prompt.md");
    }

    // ── Combination sweep ───────────────────────────────────────────
    // Belt-and-braces: renders the base template across tool-kind subsets
    // and asserts no raw template tokens leak. The static guard test above
    // is the authoritative check; this one just catches syntax drift.

    // ── is_non_interactive gating ──────────────────────────────────
    // Headless / SDK / stdio / generic-ACP sessions have no human typing
    // into a TUI prompt, so the `! <command>` shell-prefix tip and the
    // `<user_guide>` TUI pointer are noise. Those sections must drop out
    // when `is_non_interactive=true` and remain when it's false.

    #[test]
    fn interactive_renders_shell_prefix_tip_and_user_guide() {
        // The `! <command>` shell-prefix tip was removed from the minimal
        // prompt. The <user_guide> block still renders for interactive
        // sessions only, so that's what we assert here.
        let mut p = default_placeholders();
        p["is_non_interactive"] = serde_json::json!(false);
        let prompt = render_base(&default_renderer(), &p);
        assert!(
            prompt.contains("<user_guide>"),
            "interactive prompt must keep the <user_guide> block"
        );
        assert!(
            prompt.contains("interactive CLI tool"),
            "interactive prompt must declare interactive mode in the header"
        );
        assert!(
            !prompt.contains("autonomous agent"),
            "interactive prompt must NOT advertise non-interactive (autonomous) mode"
        );
    }

    #[test]
    fn non_interactive_suppresses_shell_prefix_tip_and_user_guide() {
        let mut p = default_placeholders();
        p["is_non_interactive"] = serde_json::json!(true);
        let prompt = render_base(&default_renderer(), &p);
        assert!(
            !prompt.contains("`! <command>`"),
            "non-interactive prompt must suppress the shell-prefix tip"
        );
        assert!(
            !prompt.contains("<user_guide>"),
            "non-interactive prompt must suppress the <user_guide> block"
        );
        assert!(
            prompt.contains("autonomous agent"),
            "non-interactive prompt must declare autonomous mode in the header"
        );
        assert!(
            !prompt.contains("interactive CLI tool"),
            "non-interactive prompt must NOT claim to be the interactive CLI"
        );
        // Sanity: rest of the template still renders.
        assert!(prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL));
        assert!(prompt.contains("user_query"));
    }

    #[test]
    fn test_combination_sweep_no_unresolved_variables() {
        let optional = [
            ToolKind::Read,
            ToolKind::Edit,
            ToolKind::Execute,
            ToolKind::Search,
            ToolKind::List,
            ToolKind::Plan,
            ToolKind::Skill,
            ToolKind::Task,
            ToolKind::AskUser,
            ToolKind::EnterPlan,
            ToolKind::ExitPlan,
            ToolKind::BackgroundTaskAction,
            ToolKind::Monitor,
            ToolKind::MemorySearch,
            ToolKind::MemoryGet,
        ];
        let mut subsets: Vec<Vec<ToolKind>> = vec![vec![], optional.to_vec()];
        for i in 0..optional.len() {
            subsets.push(vec![optional[i]]);
            for j in (i + 1)..optional.len() {
                subsets.push(vec![optional[i], optional[j]]);
            }
        }

        for memory_enabled in [false, true] {
            for subset in &subsets {
                let tools: HashMap<ToolKind, String> = subset
                    .iter()
                    .map(|k| (*k, format!("{k:?}").to_lowercase()))
                    .collect();
                let r = TemplateRenderer::new(tools, HashMap::new());
                let mut p = default_placeholders();
                p["memory_enabled"] = serde_json::json!(memory_enabled);
                let rendered = r
                    .render_with_extra(&base_template(), &p)
                    .unwrap_or_else(|e| {
                        panic!("render failed: {subset:?} mem={memory_enabled}: {e:?}")
                    });
                assert!(
                    !rendered.contains("${{") && !rendered.contains("${%"),
                    "unresolved token in render: {subset:?} mem={memory_enabled}",
                );
            }
        }
    }
}
