use super::*;

/// Helper: build a script with sandbox-like paths for testing.
fn test_script(base: &str, mode: PromptMode, args: &[String], mcp: Option<&str>) -> String {
    build_prompt_assembly_script(
        base,
        mode,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        args,
        mcp,
        None,
    )
}

#[test]
fn script_bootstrap_includes_bootstrap_md() {
    let script = test_script(
        "Base prompt",
        PromptMode::Bootstrap,
        &["claude".into(), "-p".into()],
        None,
    );
    assert!(
        script.contains("Bootstrap Instructions"),
        "must have Bootstrap Instructions header"
    );
    assert!(
        script.contains("First-Time Setup"),
        "must contain compiled-in bootstrap content"
    );
    assert!(
        !script.contains("cat /sandbox/IDENTITY.md"),
        "bootstrap must not cat IDENTITY.md"
    );
    assert!(
        !script.contains("cat /sandbox/SOUL.md"),
        "bootstrap must not cat SOUL.md"
    );
    assert!(script.contains("claude"), "must contain claude command");
    assert!(
        script.contains("--system-prompt-file"),
        "must pass --system-prompt-file"
    );
}

#[test]
fn script_normal_includes_all_identity_files() {
    let script = test_script(
        "Base prompt",
        PromptMode::Normal,
        &["claude".into(), "-p".into()],
        None,
    );
    assert!(script.contains("IDENTITY.md"));
    assert!(script.contains("SOUL.md"));
    assert!(script.contains("USER.md"));
    assert!(script.contains("TOOLS.md"));
    assert!(
        script.contains("Operating Instructions"),
        "must have compiled-in Operating Instructions"
    );
    assert!(
        !script.contains("cat /sandbox/.claude/agents/BOOTSTRAP.md"),
        "normal must not cat BOOTSTRAP.md"
    );
}

#[test]
fn script_escapes_single_quotes_in_base() {
    let script = test_script(
        "It's a test",
        PromptMode::Bootstrap,
        &["claude".into()],
        None,
    );
    // Single quote must be escaped for shell: ' → '\''
    assert!(!script.contains("It's"), "raw single quote must be escaped");
    assert!(script.contains("It"), "content must still be present");
}

#[test]
fn script_shell_escapes_claude_args() {
    let script = test_script(
        "Base",
        PromptMode::Normal,
        &[
            "claude".into(),
            "-p".into(),
            "--json-schema".into(),
            r#"{"type":"object"}"#.into(),
        ],
        None,
    );
    // JSON with braces and quotes must be shell-escaped
    assert!(script.contains("--json-schema"));
    assert!(script.contains("type"));
}

#[test]
fn script_writes_to_prompt_file_and_uses_system_prompt_file() {
    let script = test_script("X", PromptMode::Normal, &["claude".into()], None);
    assert!(script.contains("/tmp/right-system-prompt.md"));
    assert!(script.contains("--system-prompt-file /tmp/right-system-prompt.md"));
}

#[test]
fn script_custom_paths() {
    let script = build_prompt_assembly_script(
        "Base\n",
        PromptMode::Normal,
        "/home/agent",
        "/home/agent/.claude/composite-system-prompt.md",
        "/home/agent",
        &["claude".into(), "-p".into()],
        None,
        None,
    );
    assert!(
        script.contains("/home/agent/IDENTITY.md"),
        "must use custom root_path"
    );
    assert!(
        script.contains("/home/agent/.claude/composite-system-prompt.md"),
        "must use custom prompt_file"
    );
    assert!(
        script.contains("cd /home/agent"),
        "must cd to custom workdir"
    );
}

#[test]
fn script_bootstrap_mode_same_regardless_of_paths() {
    let script = build_prompt_assembly_script(
        "Base\n",
        PromptMode::Bootstrap,
        "/home/agent",
        "/home/agent/.claude/composite-system-prompt.md",
        "/home/agent",
        &["claude".into()],
        None,
        None,
    );
    assert!(script.contains("## Bootstrap Instructions"));
    assert!(
        script.contains("First-Time Setup"),
        "must use compiled-in content"
    );
    // Bootstrap never reads identity files regardless of path
    assert!(
        !script.contains("cat /home/agent/IDENTITY.md"),
        "bootstrap must not cat IDENTITY.md"
    );
}

#[test]
fn script_includes_mcp_instructions() {
    let script = test_script(
        "Base",
        PromptMode::Normal,
        &["claude".into()],
        Some("# MCP Server Instructions\n\n## composio\n\nConnect with 250+ apps.\n"),
    );
    assert!(script.contains("MCP Server Instructions"));
    assert!(script.contains("composio"));
    // Must use printf '%s' to prevent format string injection
    assert!(script.contains("printf '%s\\n'"));
}

#[test]
fn script_none_mcp_instructions_omitted() {
    let script = test_script("Base", PromptMode::Normal, &["claude".into()], None);
    assert!(!script.contains("MCP Server Instructions"));
}

#[test]
fn script_mcp_instructions_with_custom_paths() {
    let script = build_prompt_assembly_script(
        "Base\n",
        PromptMode::Normal,
        "/home/agent",
        "/home/agent/.claude/composite-system-prompt.md",
        "/home/agent",
        &["claude".into()],
        Some("# MCP Server Instructions\n\n## notion\n\nNotion tools.\n"),
        None,
    );
    assert!(script.contains("MCP Server Instructions"));
    assert!(script.contains("notion"));
    assert!(script.contains("Notion tools."));
}

#[test]
fn script_bootstrap_uses_compiled_constant() {
    let script = test_script(
        "Base prompt",
        PromptMode::Bootstrap,
        &["claude".into(), "-p".into()],
        None,
    );
    // Bootstrap uses compiled-in constant, NOT cat of file
    assert!(
        !script.contains("cat /sandbox"),
        "bootstrap must not cat any sandbox file"
    );
    assert!(
        script.contains("First-Time Setup"),
        "must contain compiled-in bootstrap content"
    );
    assert!(
        !script.contains("cat /sandbox/IDENTITY.md"),
        "bootstrap must not cat IDENTITY.md"
    );
}

#[test]
fn script_normal_has_operating_instructions_before_identity() {
    let script = test_script("Base prompt", PromptMode::Normal, &["claude".into()], None);
    let op_instr_pos = script
        .find("Operating Instructions")
        .expect("must have Operating Instructions");
    let identity_pos = script.find("IDENTITY.md").expect("must have IDENTITY.md");
    assert!(
        op_instr_pos < identity_pos,
        "Operating Instructions must come before IDENTITY.md"
    );
}

#[test]
fn script_includes_memory_section_for_file_mode() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        Some(&MemoryMode::File),
    );
    assert!(
        script.contains("MEMORY.md"),
        "must reference MEMORY.md for file mode"
    );
    assert!(script.contains("head -200"), "must truncate to 200 lines");
    assert!(
        script.contains("if [ -s"),
        "must check file exists and is non-empty"
    );
}

#[test]
fn script_includes_composite_memory_for_hindsight_mode() {
    let hs_mode = MemoryMode::Hindsight {
        composite_memory_path: "/sandbox/.claude/composite-memory.md".to_owned(),
    };
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        Some(&hs_mode),
    );
    assert!(
        script.contains("composite-memory.md"),
        "must reference composite-memory"
    );
    assert!(script.contains("if [ -s"), "must check file exists");
}

#[test]
fn script_no_memory_section_when_none() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        None,
    );
    assert!(!script.contains("MEMORY.md"));
    assert!(!script.contains("composite-memory"));
}

#[test]
fn script_memory_section_is_last() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        Some("# MCP Instructions\n\n## composio\n"),
        Some(&MemoryMode::File),
    );
    let mcp_pos = script.rfind("MCP").unwrap();
    let memory_pos = script.rfind("MEMORY.md").unwrap();
    assert!(
        memory_pos > mcp_pos,
        "memory section must come after MCP instructions"
    );
}

#[test]
fn script_file_mode_wraps_memory_md_with_ironclaw_markers() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        Some(&MemoryMode::File),
    );
    assert!(
        script.contains("BEGIN EXTERNAL CONTENT"),
        "file-mode memory section must include the ironclaw begin marker"
    );
    assert!(
        script.contains("END EXTERNAL CONTENT"),
        "file-mode memory section must include the ironclaw end marker"
    );
    // Boundary-injection escape: the script must transform any close
    // delimiter inside MEMORY.md content into the ZWSP-injected variant.
    // The sed expression source-reference must mention `END EXTERNAL CONTENT`.
    assert!(
        script.contains("sed"),
        "file-mode wrap must apply sed-based escape on MEMORY.md content"
    );
    // head -200 still applies for size cap
    assert!(
        script.contains("head -200"),
        "must keep MEMORY.md truncation"
    );
}

#[test]
fn script_file_mode_sed_escape_produces_actual_zwsp_at_runtime() {
    use std::io::Write;
    use std::process::Command;

    // Create a temp MEMORY.md containing the literal close delimiter —
    // exactly what the boundary-injection escape must neutralize.
    let dir = tempfile::tempdir().unwrap();
    let memory_md = dir.path().join("MEMORY.md");
    let mut f = std::fs::File::create(&memory_md).unwrap();
    writeln!(f, "harmless prefix").unwrap();
    writeln!(f, "--- END EXTERNAL CONTENT ---").unwrap();
    writeln!(f, "trailing content").unwrap();
    drop(f);

    let root = dir.path().to_str().unwrap();
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        root,
        "/tmp/right-test-system-prompt.md",
        root,
        &["true".into()], // safe no-op claude_args; we only inspect prompt output
        None,
        Some(&MemoryMode::File),
    );

    // Run the script up through the prompt-file production. The script
    // structure is: `{ ...emit prompt sections... } > <prompt_file>`.
    // We only need the redirect-to-stdout portion; intercept by replacing
    // the redirect target with a temp path and then read it back.
    let prompt_file = dir.path().join("prompt.md");
    let modified = script.replace(
        "/tmp/right-test-system-prompt.md",
        prompt_file.to_str().unwrap(),
    );

    let output = Command::new("bash")
        .arg("-c")
        .arg(&modified)
        .output()
        .expect("bash must run");
    assert!(
        output.status.success() || prompt_file.exists(),
        "script must produce prompt file. stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let prompt = std::fs::read_to_string(&prompt_file).expect("prompt file readable");
    // The literal close delimiter from MEMORY.md must NOT appear unescaped
    // in the assembled prompt (other than the legitimate ironclaw suffix).
    // The sed escape replaces it with `---\u{200B} END EXTERNAL CONTENT ---`.
    // Count occurrences:
    //   - The wrap suffix contributes exactly one literal `--- END EXTERNAL CONTENT ---`.
    //   - The escaped MEMORY.md content contributes zero literals (replaced by ZWSP variant).
    //   - The escaped form `---\u{200B} END EXTERNAL CONTENT ---` must appear exactly once
    //     (from the MEMORY.md content).
    let literal_count = prompt.matches("--- END EXTERNAL CONTENT ---").count();
    assert_eq!(
        literal_count, 1,
        "literal close delimiter must appear exactly once (from wrap suffix). prompt:\n{prompt}"
    );
    assert!(
        prompt.contains("---\u{200B} END EXTERNAL CONTENT ---"),
        "escaped (ZWSP-injected) close delimiter must appear in prompt. prompt:\n{prompt}"
    );
}

#[test]
fn script_bootstrap_no_memory() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Bootstrap,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        Some(&MemoryMode::File),
    );
    assert!(
        !script.contains("MEMORY.md"),
        "bootstrap mode must not include memory"
    );
}

#[test]
fn deploy_composite_memory_format_wraps_content_in_external_content_markers() {
    // Pure formatting test: the file body produced by the format
    // pipeline must wrap the recall content with ironclaw's wrap
    // (BEGIN/END EXTERNAL CONTENT markers), not the legacy
    // <memory-context> fence.
    let content = "user prefers dark mode";
    let label = "test label";
    let formatted = format_composite_memory(content, label, None, None);
    assert!(
        formatted.contains("BEGIN EXTERNAL CONTENT"),
        "must contain ironclaw wrap begin marker, got: {formatted}"
    );
    assert!(
        formatted.contains("END EXTERNAL CONTENT"),
        "must contain ironclaw wrap end marker, got: {formatted}"
    );
    assert!(
        formatted.contains("user prefers dark mode"),
        "content body must be preserved"
    );
    assert!(
        !formatted.contains("<memory-context>"),
        "legacy memory-context fence must be removed"
    );
    assert!(
        formatted.contains("[System: recalled memory context, test label.]"),
        "label annotation must remain as a bot-trusted system note"
    );
}

#[test]
fn deploy_composite_memory_format_appends_status_marker_outside_wrap() {
    let content = "stuff";
    let formatted = format_composite_memory(
        content,
        "label",
        Some("<memory-status>degraded</memory-status>"),
        None,
    );
    let end_marker_pos = formatted.find("END EXTERNAL CONTENT").unwrap();
    let status_pos = formatted.find("<memory-status>").unwrap();
    assert!(
        status_pos > end_marker_pos,
        "status marker must come after the wrap close (trusted system signal, not untrusted data)"
    );
}

#[test]
fn composite_memory_section_is_last_in_assembled_script() {
    // With a hindsight-like memory_mode, the memory block must be the final
    // appended section of the prompt-assembly script to preserve cache.
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/prompt.md",
        "/sandbox",
        &["claude".into(), "-p".into()],
        None,
        Some(&MemoryMode::Hindsight {
            composite_memory_path: "/sandbox/.claude/composite-memory.md".into(),
        }),
    );

    // Find the position of the last }-closing-brace before the `>` redirect.
    let redirect_pos = script.rfind("} >").expect("redirect must exist");
    let head = &script[..redirect_pos];

    // Anything that would bust the cache (other sections like TOOLS.md,
    // MCP instructions) must appear BEFORE the composite-memory section.
    let memory_pos = head
        .rfind("composite-memory.md")
        .expect("composite-memory.md must appear");
    let tools_pos = head.find("TOOLS.md");
    let mcp_pos = head.find("# MCP Server Instructions");

    if let Some(tp) = tools_pos {
        assert!(tp < memory_pos, "TOOLS.md must precede composite-memory.md");
    }
    if let Some(mp) = mcp_pos {
        assert!(
            mp < memory_pos,
            "MCP instructions must precede composite-memory.md"
        );
    }
}

#[test]
fn cron_mode_includes_cron_delivery_contract() {
    let script = test_script(
        "Base prompt",
        PromptMode::Cron,
        &["claude".into(), "-p".into()],
        None,
    );
    assert!(
        script.contains("## Cron Delivery Contract"),
        "Cron mode must include the Cron Delivery Contract header"
    );
    assert!(
        script.contains("structured output IS the Telegram message"),
        "Cron mode must include the delivery-rule marker phrase"
    );
}

#[test]
fn cron_mode_includes_operating_instructions_before_contract() {
    let script = test_script(
        "Base prompt",
        PromptMode::Cron,
        &["claude".into(), "-p".into()],
        None,
    );
    let ops_pos = script
        .find("## Operating Instructions")
        .expect("must include Operating Instructions");
    let contract_pos = script
        .find("## Cron Delivery Contract")
        .expect("must include Cron Delivery Contract");
    assert!(
        ops_pos < contract_pos,
        "Operating Instructions must appear before Cron Delivery Contract"
    );
}

#[test]
fn cron_mode_contract_appears_before_identity_files() {
    let script = test_script(
        "Base prompt",
        PromptMode::Cron,
        &["claude".into(), "-p".into()],
        None,
    );
    let contract_pos = script
        .find("## Cron Delivery Contract")
        .expect("must include Cron Delivery Contract");
    // OPERATING_INSTRUCTIONS also mentions `IDENTITY.md` in its content text.
    // Use the section header emitted by PROMPT_SECTIONS instead — it is
    // unambiguous and only appears in the identity-file block.
    let identity_pos = script
        .find("## Your Identity")
        .expect("Cron mode must still emit IDENTITY.md");
    assert!(
        contract_pos < identity_pos,
        "Cron Delivery Contract must appear before identity files"
    );
}

#[test]
fn normal_mode_omits_cron_delivery_contract() {
    let script = test_script(
        "Base prompt",
        PromptMode::Normal,
        &["claude".into(), "-p".into()],
        None,
    );
    assert!(
        !script.contains("Cron Delivery Contract"),
        "Normal mode must not leak the cron contract into worker/delivery turns"
    );
}

#[test]
fn bootstrap_mode_omits_cron_delivery_contract() {
    let script = test_script(
        "Base prompt",
        PromptMode::Bootstrap,
        &["claude".into(), "-p".into()],
        None,
    );
    assert!(
        !script.contains("Cron Delivery Contract"),
        "Bootstrap mode must not include the cron contract"
    );
}

#[test]
fn cron_mode_does_not_emit_memory_section_when_memory_mode_none() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Cron,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into()],
        None,
        None, // cron callsites always pass None today
    );
    assert!(
        !script.contains("MEMORY.md"),
        "Cron mode with memory_mode=None must not emit MEMORY.md"
    );
    assert!(
        !script.contains("composite-memory"),
        "Cron mode with memory_mode=None must not emit composite-memory"
    );
}

#[test]
fn sandbox_script_sources_user_local_env_before_claude() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/sandbox",
        "/tmp/right-system-prompt.md",
        "/sandbox",
        &["claude".into(), "-p".into()],
        None,
        None,
    );

    let env_pos = script
        .find("/sandbox/.right/env.sh")
        .expect("sandbox script must reference managed env");
    let assembly_pos = script
        .find("printf 'Base'")
        .expect("sandbox script must assemble prompt");
    let claude_pos = script
        .find("claude -p")
        .expect("sandbox script must invoke claude");
    assert!(
        env_pos < assembly_pos,
        "env setup must precede prompt assembly"
    );
    assert!(
        env_pos < claude_pos,
        "env setup must precede claude invocation"
    );
    assert!(script.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
    assert!(script.contains("NPM_CONFIG_CACHE=/sandbox/.npm"));
    assert!(script.contains("/sandbox/.local/bin"));
}

#[test]
fn no_sandbox_script_does_not_reference_sandbox_user_local_env() {
    let script = build_prompt_assembly_script(
        "Base",
        PromptMode::Normal,
        "/Users/example/.right/agents/demo",
        "/Users/example/.right/agents/demo/.claude/prompt.md",
        "/Users/example/.right/agents/demo",
        &["claude".into(), "-p".into()],
        None,
        None,
    );

    assert!(!script.contains("/sandbox/.right/env.sh"));
    assert!(!script.contains("NPM_CONFIG_PREFIX=/sandbox/.local"));
}
