use std::path::{Path, PathBuf};

use tempfile::tempdir;

use right_agent_config::{AgentConfig, AgentDef, RestartPolicy, SandboxConfig, SandboxMode};

use crate::{ProcessComposeConfig, generate_process_compose};

const EXE_PATH: &str = "/usr/bin/right";
const CLOUDFLARED_SCRIPT: &str = "/home/user/.right/scripts/cloudflared-start.sh";

fn default_config() -> ProcessComposeConfig<'static> {
    ProcessComposeConfig {
        debug: false,
        home: Path::new("/home/user/.right"),
        cloudflared_script: Path::new(CLOUDFLARED_SCRIPT),
        token_map_path: None,
    }
}

fn make_bot_agent(name: &str, token: &str) -> AgentDef {
    let config = Some(AgentConfig {
        restart: RestartPolicy::OnFailure,
        max_restarts: 3,
        backoff_seconds: 5,
        network_policy: Default::default(),
        model: None,
        debug: None,
        sandbox: Some(SandboxConfig {
            mode: SandboxMode::None,
            policy_file: None,
            name: None,
        }),
        telegram_token: Some(token.to_string()),

        allowed_chat_ids: vec![],
        env: std::collections::HashMap::new(),
        secret: None,
        attachments: Default::default(),
        show_thinking: true,
        learning: Default::default(),
        memory: None,
        stt: Default::default(),
    });
    AgentDef {
        name: name.to_owned(),
        path: PathBuf::from(format!("/home/user/.right/agents/{name}")),
        identity_path: PathBuf::from(format!("/home/user/.right/agents/{name}/IDENTITY.md")),
        config,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }
}

fn make_agent_no_token(name: &str) -> AgentDef {
    let config = Some(AgentConfig {
        restart: RestartPolicy::OnFailure,
        max_restarts: 3,
        backoff_seconds: 5,
        network_policy: Default::default(),
        model: None,
        debug: None,
        sandbox: Some(SandboxConfig {
            mode: SandboxMode::None,
            policy_file: None,
            name: None,
        }),
        telegram_token: None,

        allowed_chat_ids: vec![],
        env: std::collections::HashMap::new(),
        secret: None,
        attachments: Default::default(),
        show_thinking: true,
        learning: Default::default(),
        memory: None,
        stt: Default::default(),
    });
    AgentDef {
        name: name.to_owned(),
        path: PathBuf::from(format!("/home/user/.right/agents/{name}")),
        identity_path: PathBuf::from(format!("/home/user/.right/agents/{name}/IDENTITY.md")),
        config,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }
}

fn make_agent_no_config(name: &str) -> AgentDef {
    AgentDef {
        name: name.to_owned(),
        path: PathBuf::from(format!("/home/user/.right/agents/{name}")),
        identity_path: PathBuf::from(format!("/home/user/.right/agents/{name}/IDENTITY.md")),
        config: None,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }
}

fn make_agent_with_restart(name: &str, token: &str, restart: RestartPolicy) -> AgentDef {
    let config = Some(AgentConfig {
        restart,
        max_restarts: 5,
        backoff_seconds: 10,
        network_policy: Default::default(),
        model: None,
        debug: None,
        sandbox: Some(SandboxConfig {
            mode: SandboxMode::None,
            policy_file: None,
            name: None,
        }),
        telegram_token: Some(token.to_string()),

        allowed_chat_ids: vec![],
        env: std::collections::HashMap::new(),
        secret: None,
        attachments: Default::default(),
        show_thinking: true,
        learning: Default::default(),
        memory: None,
        stt: Default::default(),
    });
    AgentDef {
        name: name.to_owned(),
        path: PathBuf::from(format!("/home/user/.right/agents/{name}")),
        identity_path: PathBuf::from(format!("/home/user/.right/agents/{name}/IDENTITY.md")),
        config,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }
}

// ── Bot process key ─────────────────────────────────────────────────────────

#[test]
fn bot_agent_process_key_contains_name_bot() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("myagent-bot:"),
        "expected '<name>-bot:' process key in:\n{output}"
    );
}

#[test]
fn bot_agent_command_contains_right_bot_agent() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("right bot --agent myagent"),
        "expected 'right bot --agent myagent' in:\n{output}"
    );
}

#[test]
fn bot_agent_env_contains_rc_agent_dir() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_AGENT_DIR=/home/user/.right/agents/myagent"),
        "expected RC_AGENT_DIR in:\n{output}"
    );
}

#[test]
fn bot_agent_env_contains_rc_agent_name() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_AGENT_NAME=myagent"),
        "expected RC_AGENT_NAME=myagent in:\n{output}"
    );
}

#[test]
fn token_not_leaked_to_process_compose_env() {
    let agents = vec![make_bot_agent("myagent", "999:mytoken")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        !output.contains("RC_TELEGRAM_TOKEN"),
        "RC_TELEGRAM_TOKEN must not appear in process-compose output:\n{output}"
    );
    // Agent with token still produces a process entry
    assert!(
        output.contains("myagent-bot:"),
        "agent with token must still appear in output:\n{output}"
    );
}

// ── Non-telegram agents absent from output ──────────────────────────────────

#[test]
fn agent_without_telegram_token_absent_from_output() {
    let agents = vec![
        make_bot_agent("with-token", "123:tok"),
        make_agent_no_token("no-token"),
    ];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        !output.contains("no-token"),
        "agent without token must be absent from output:\n{output}"
    );
}

#[test]
fn agent_without_config_absent_from_output() {
    let agents = vec![
        make_bot_agent("with-token", "123:tok"),
        make_agent_no_config("no-config"),
    ];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        !output.contains("no-config"),
        "agent without config must be absent from output:\n{output}"
    );
}

// ── No is_interactive anywhere ───────────────────────────────────────────────

#[test]
fn output_does_not_contain_is_interactive() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        !output.contains("is_interactive"),
        "is_interactive must not appear in output:\n{output}"
    );
}

// ── MCP env vars ────────────────────────────────────────────────────────────

#[test]
fn env_contains_enable_claudeai_mcp_servers_false() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("ENABLE_CLAUDEAI_MCP_SERVERS=false"),
        "expected ENABLE_CLAUDEAI_MCP_SERVERS=false in:\n{output}"
    );
}

// ENABLE_TOOL_SEARCH and MCP_CONNECTION_NONBLOCKING were removed:
// - ENABLE_TOOL_SEARCH=false blocked ToolSearch for all tools including MCP discovery
// - MCP_CONNECTION_NONBLOCKING=1 is unnecessary — synchronous MCP connect is preferred

// ── Header and version ───────────────────────────────────────────────────────

#[test]
fn output_starts_with_generated_comment() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.starts_with("# Generated by right"),
        "expected '# Generated by right' at start of:\n{output}"
    );
}

#[test]
fn output_contains_is_strict_true() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("is_strict: true"),
        "expected is_strict: true in:\n{output}"
    );
}

// ── Restart policies ─────────────────────────────────────────────────────────

#[test]
fn restart_policy_on_failure_maps_correctly() {
    let agents = vec![make_agent_with_restart(
        "bot",
        "123:tok",
        RestartPolicy::OnFailure,
    )];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("restart: \"on_failure\""),
        "expected on_failure policy in:\n{output}"
    );
}

#[test]
fn restart_policy_always_maps_correctly() {
    let agents = vec![make_agent_with_restart(
        "bot",
        "123:tok",
        RestartPolicy::Always,
    )];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("restart: \"always\""),
        "expected always policy in:\n{output}"
    );
}

#[test]
fn restart_policy_never_maps_to_no() {
    let agents = vec![make_agent_with_restart(
        "bot",
        "123:tok",
        RestartPolicy::Never,
    )];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("restart: \"no\""),
        "expected 'no' for Never policy in:\n{output}"
    );
}

#[test]
fn defaults_when_no_config_not_in_output() {
    // Agent with no config has no telegram token, so should not appear in output at all
    let agents = vec![make_agent_no_config("plain")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    // No bot agents => the processes section should be empty (no plain: entry)
    assert!(
        !output.contains("plain"),
        "agent without config (no token) must not appear in output:\n{output}"
    );
}

// ── Cloudflared tunnel process ───────────────────────────────────────────────

#[test]
fn cloudflared_always_produces_process_entry() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("  cloudflared:"),
        "expected cloudflared process key in:\n{output}"
    );
    assert!(
        output.contains("command: \"/home/user/.right/scripts/cloudflared-start.sh\""),
        "expected absolute script path in cloudflared command:\n{output}"
    );
    assert!(
        output.contains("working_dir: \"/home/user/.right\""),
        "expected home dir as working_dir:\n{output}"
    );
    assert!(
        output.contains("backoff_seconds: 5"),
        "expected backoff_seconds: 5:\n{output}"
    );
    assert!(
        output.contains("max_restarts: 10"),
        "expected max_restarts: 10:\n{output}"
    );
    assert!(
        output.contains("timeout_seconds: 30"),
        "expected timeout_seconds: 30:\n{output}"
    );
}

#[test]
fn bot_depends_on_cloudflared() {
    let agents = vec![make_bot_agent("myagent", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    // Per-agent bot must declare a depends_on entry on cloudflared.
    assert!(
        output.contains("cloudflared:\n        condition: process_started"),
        "expected bot depends_on cloudflared in:\n{output}"
    );
}

// ── Sandbox mode env vars ───────────────────────────────────────────────────

fn make_agent_with_sandbox(
    name: &str,
    token: &str,
    mode: SandboxMode,
    policy_file: Option<&str>,
) -> AgentDef {
    let config = Some(AgentConfig {
        restart: RestartPolicy::OnFailure,
        max_restarts: 3,
        backoff_seconds: 5,
        network_policy: Default::default(),
        model: None,
        debug: None,
        sandbox: Some(SandboxConfig {
            mode,
            policy_file: policy_file.map(std::path::PathBuf::from),
            name: None,
        }),
        telegram_token: Some(token.to_string()),
        allowed_chat_ids: vec![],
        env: std::collections::HashMap::new(),
        secret: None,
        attachments: Default::default(),
        show_thinking: true,
        learning: Default::default(),
        memory: None,
        stt: Default::default(),
    });
    AgentDef {
        name: name.to_owned(),
        path: PathBuf::from(format!("/home/user/.right/agents/{name}")),
        identity_path: PathBuf::from(format!("/home/user/.right/agents/{name}/IDENTITY.md")),
        config,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }
}

#[test]
fn per_agent_sandbox_openshell_emits_openshell_mode() {
    let agents = vec![make_agent_with_sandbox(
        "sandboxed",
        "123:tok",
        SandboxMode::Openshell,
        Some("policy.yaml"),
    )];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_SANDBOX_MODE=openshell"),
        "expected RC_SANDBOX_MODE=openshell:\n{output}"
    );
    assert!(
        output.contains("RC_SANDBOX_POLICY=/home/user/.right/agents/sandboxed/policy.yaml"),
        "expected policy path:\n{output}"
    );
    assert!(
        !output.contains("--no-sandbox"),
        "--no-sandbox must not appear:\n{output}"
    );
}

#[test]
fn per_agent_sandbox_none_emits_none_mode() {
    let agents = vec![make_agent_with_sandbox(
        "unsandboxed",
        "123:tok",
        SandboxMode::None,
        None,
    )];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_SANDBOX_MODE=none"),
        "expected RC_SANDBOX_MODE=none:\n{output}"
    );
    assert!(
        !output.contains("RC_SANDBOX_POLICY"),
        "RC_SANDBOX_POLICY must be absent:\n{output}"
    );
}

#[test]
fn mixed_sandbox_modes_in_same_config() {
    let agents = vec![
        make_agent_with_sandbox(
            "sandboxed",
            "123:tok",
            SandboxMode::Openshell,
            Some("policy.yaml"),
        ),
        make_agent_with_sandbox("direct", "456:tok", SandboxMode::None, None),
    ];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(output.contains("sandboxed-bot:"));
    assert!(output.contains("direct-bot:"));
    assert!(output.contains("RC_SANDBOX_MODE=openshell"));
    assert!(output.contains("RC_SANDBOX_MODE=none"));
}

// ── Login process ───────────────────────────────────────────────────────────

// ── RC_PC_PORT env var ──────────────────────────────────────────────────────

#[test]
fn bot_process_has_rc_pc_port_env() {
    let agents = vec![make_bot_agent("right", "123:tok")];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_PC_PORT="),
        "expected RC_PC_PORT env var on bot process:\n{output}"
    );
}

// ── right-mcp-server process ─────────────────────────────────────────────────

#[test]
fn right_mcp_server_process_included_when_token_map_provided() {
    let dir = tempdir().unwrap();
    let token_map = dir.path().join("agent-tokens.json");
    std::fs::write(&token_map, "{}").unwrap();
    let agents = vec![make_bot_agent("test", "123:tok")];
    let yaml = generate_process_compose(
        &agents,
        Path::new("/usr/bin/right"),
        &ProcessComposeConfig {
            home: dir.path(),
            token_map_path: Some(&token_map),
            ..default_config()
        },
    )
    .unwrap();
    assert!(
        yaml.contains("right-mcp-server:"),
        "must have right-mcp-server process"
    );
    assert!(yaml.contains("mcp-server"), "must run mcp-server command");
    assert!(yaml.contains("--port 8100"), "must specify port");
    assert!(
        yaml.contains("depends_on:"),
        "bot must depend on mcp server"
    );
}

#[test]
fn mixed_mode_agents_correct_env_vars() {
    let agents = vec![
        make_agent_with_sandbox(
            "coder",
            "111:tok",
            SandboxMode::Openshell,
            Some("policy.yaml"),
        ),
        make_agent_with_sandbox("browser", "222:tok", SandboxMode::None, None),
        make_agent_with_sandbox(
            "reviewer",
            "333:tok",
            SandboxMode::Openshell,
            Some("custom-policy.yaml"),
        ),
    ];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();

    // coder: sandboxed
    assert!(output.contains("coder-bot:"));
    assert!(output.contains("RC_SANDBOX_POLICY=/home/user/.right/agents/coder/policy.yaml"));

    // browser: unsandboxed — should not have RC_SANDBOX_POLICY in its section
    assert!(output.contains("browser-bot:"));

    // reviewer: sandboxed with custom policy
    assert!(
        output.contains("RC_SANDBOX_POLICY=/home/user/.right/agents/reviewer/custom-policy.yaml")
    );
}

#[test]
fn agent_without_sandbox_config_defaults_to_openshell_in_process_compose() {
    let config = Some(AgentConfig {
        restart: RestartPolicy::OnFailure,
        max_restarts: 3,
        backoff_seconds: 5,
        network_policy: Default::default(),
        model: None,
        debug: None,
        sandbox: None, // absent from yaml → default openshell
        telegram_token: Some("123:tok".to_string()),
        allowed_chat_ids: vec![],
        env: std::collections::HashMap::new(),
        secret: None,
        attachments: Default::default(),
        show_thinking: true,
        learning: Default::default(),
        memory: None,
        stt: Default::default(),
    });
    let agents = vec![AgentDef {
        name: "default-agent".to_owned(),
        path: PathBuf::from("/home/user/.right/agents/default-agent"),
        identity_path: PathBuf::from("/home/user/.right/agents/default-agent/IDENTITY.md"),
        config,
        soul_path: None,
        user_path: None,
        tools_path: None,
        bootstrap_path: None,
        heartbeat_path: None,
    }];
    let exe = Path::new(EXE_PATH);
    let output = generate_process_compose(&agents, exe, &default_config()).unwrap();
    assert!(
        output.contains("RC_SANDBOX_MODE=openshell"),
        "agent without explicit sandbox config should default to openshell:\n{output}"
    );
}
