use std::path::PathBuf;
use std::sync::Arc;

use codex_protocol::models::ShellCommandToolCallParams;
use core_test_support::PathBufExt;
use core_test_support::test_path_buf;
use pretty_assertions::assert_eq;

use crate::exec_env::create_env;
use crate::sandboxing::SandboxPermissions;
use crate::session::tests::make_session_and_context;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::shell_snapshot::ShellSnapshot;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellHandler;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::ToolHandler;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_shell_command::powershell::try_find_powershell_executable_blocking;
use codex_shell_command::powershell::try_find_pwsh_executable_blocking;
use serde_json::json;
use tokio::sync::Mutex;
use tokio::sync::watch;

/// The logic for is_known_safe_command() has heuristics for known shells,
/// so we must ensure the commands generated by [ShellCommandHandler] can be
/// recognized as safe if the `command` is safe.
#[test]
fn commands_generated_by_shell_command_handler_can_be_matched_by_is_known_safe_command() {
    let bash_shell = Shell {
        shell_type: ShellType::Bash,
        shell_path: PathBuf::from("/bin/bash"),
        shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
    };
    assert_safe(&bash_shell, "ls -la");

    let zsh_shell = Shell {
        shell_type: ShellType::Zsh,
        shell_path: PathBuf::from("/bin/zsh"),
        shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
    };
    assert_safe(&zsh_shell, "ls -la");

    if let Some(path) = try_find_powershell_executable_blocking() {
        let powershell = Shell {
            shell_type: ShellType::PowerShell,
            shell_path: path.to_path_buf(),
            shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
        };
        assert_safe(&powershell, "ls -Name");
    }

    if let Some(path) = try_find_pwsh_executable_blocking() {
        let pwsh = Shell {
            shell_type: ShellType::PowerShell,
            shell_path: path.to_path_buf(),
            shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
        };
        assert_safe(&pwsh, "ls -Name");
    }
}

fn assert_safe(shell: &Shell, command: &str) {
    assert!(is_known_safe_command(&shell.derive_exec_args(
        command, /* use_login_shell */ /*use_login_shell*/ true
    )));
    assert!(is_known_safe_command(&shell.derive_exec_args(
        command, /* use_login_shell */ /*use_login_shell*/ false
    )));
}

#[tokio::test]
async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() {
    let (session, turn_context) = make_session_and_context().await;

    let command = "echo hello".to_string();
    let workdir = Some("subdir".to_string());
    let login = None;
    let timeout_ms = Some(1234);
    let sandbox_permissions = SandboxPermissions::RequireEscalated;
    let justification = Some("because tests".to_string());

    let expected_command = session
        .user_shell()
        .derive_exec_args(&command, /*use_login_shell*/ true);
    let expected_cwd = turn_context.resolve_path(workdir.clone());
    let expected_env = create_env(
        &turn_context.shell_environment_policy,
        Some(session.conversation_id),
    );

    let params = ShellCommandToolCallParams {
        command,
        workdir,
        login,
        timeout_ms,
        sandbox_permissions: Some(sandbox_permissions),
        additional_permissions: None,
        prefix_rule: None,
        justification: justification.clone(),
    };

    let exec_params = ShellCommandHandler::to_exec_params(
        &params,
        &session,
        &turn_context,
        session.conversation_id,
        /*allow_login_shell*/ true,
    )
    .expect("login shells should be allowed");

    // ExecParams cannot derive Eq due to the CancellationToken field, so we manually compare the fields.
    assert_eq!(exec_params.command, expected_command);
    assert_eq!(exec_params.cwd, expected_cwd);
    assert_eq!(exec_params.env, expected_env);
    assert_eq!(exec_params.network, turn_context.network);
    assert_eq!(exec_params.expiration.timeout_ms(), timeout_ms);
    assert_eq!(exec_params.sandbox_permissions, sandbox_permissions);
    assert_eq!(exec_params.justification, justification);
    assert_eq!(exec_params.arg0, None);
}

#[test]
fn shell_command_handler_respects_explicit_login_flag() {
    let (_tx, shell_snapshot) = watch::channel(Some(Arc::new(ShellSnapshot {
        path: test_path_buf("/tmp/snapshot.sh").abs(),
        cwd: test_path_buf("/tmp").abs(),
    })));
    let shell = Shell {
        shell_type: ShellType::Bash,
        shell_path: PathBuf::from("/bin/bash"),
        shell_snapshot,
    };

    let login_command = ShellCommandHandler::base_command(
        &shell,
        "echo login shell",
        /*use_login_shell*/ true,
    );
    assert_eq!(
        login_command,
        shell.derive_exec_args("echo login shell", /*use_login_shell*/ true)
    );

    let non_login_command = ShellCommandHandler::base_command(
        &shell,
        "echo non login shell",
        /*use_login_shell*/ false,
    );
    assert_eq!(
        non_login_command,
        shell.derive_exec_args("echo non login shell", /*use_login_shell*/ false)
    );
}

#[tokio::test]
async fn shell_command_handler_defaults_to_non_login_when_disallowed() {
    let (session, turn_context) = make_session_and_context().await;
    let params = ShellCommandToolCallParams {
        command: "echo hello".to_string(),
        workdir: None,
        login: None,
        timeout_ms: None,
        sandbox_permissions: None,
        additional_permissions: None,
        prefix_rule: None,
        justification: None,
    };

    let exec_params = ShellCommandHandler::to_exec_params(
        &params,
        &session,
        &turn_context,
        session.conversation_id,
        /*allow_login_shell*/ false,
    )
    .expect("non-login shells should still be allowed");

    assert_eq!(
        exec_params.command,
        session
            .user_shell()
            .derive_exec_args("echo hello", /*use_login_shell*/ false)
    );
}

#[test]
fn shell_command_handler_rejects_login_when_disallowed() {
    let err =
        ShellCommandHandler::resolve_use_login_shell(Some(true), /*allow_login_shell*/ false)
            .expect_err("explicit login should be rejected");

    assert!(
        err.to_string()
            .contains("login shell is disabled by config"),
        "unexpected error: {err}"
    );
}

#[tokio::test]
async fn shell_pre_tool_use_payload_uses_joined_command() {
    let payload = ToolPayload::LocalShell {
        params: codex_protocol::models::ShellToolCallParams {
            command: vec![
                "bash".to_string(),
                "-lc".to_string(),
                "printf hi".to_string(),
            ],
            workdir: None,
            timeout_ms: None,
            sandbox_permissions: None,
            prefix_rule: None,
            additional_permissions: None,
            justification: None,
        },
    };
    let (session, turn) = make_session_and_context().await;
    let handler = ShellHandler;

    assert_eq!(
        handler.pre_tool_use_payload(&ToolInvocation {
            session: session.into(),
            turn: turn.into(),
            cancellation_token: tokio_util::sync::CancellationToken::new(),
            tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
            call_id: "call-41".to_string(),
            tool_name: codex_tools::ToolName::plain("shell"),
            source: crate::tools::context::ToolCallSource::Direct,
            payload,
        }),
        Some(crate::tools::registry::PreToolUsePayload {
            tool_name: HookToolName::bash(),
            tool_input: json!({ "command": "bash -lc 'printf hi'" }),
        })
    );
}

#[tokio::test]
async fn shell_command_pre_tool_use_payload_uses_raw_command() {
    let payload = ToolPayload::Function {
        arguments: json!({ "command": "printf shell command" }).to_string(),
    };
    let (session, turn) = make_session_and_context().await;
    let handler = ShellCommandHandler {
        backend: super::ShellCommandBackend::Classic,
    };

    assert_eq!(
        handler.pre_tool_use_payload(&ToolInvocation {
            session: session.into(),
            turn: turn.into(),
            cancellation_token: tokio_util::sync::CancellationToken::new(),
            tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
            call_id: "call-42".to_string(),
            tool_name: codex_tools::ToolName::plain("shell_command"),
            source: crate::tools::context::ToolCallSource::Direct,
            payload,
        }),
        Some(crate::tools::registry::PreToolUsePayload {
            tool_name: HookToolName::bash(),
            tool_input: json!({ "command": "printf shell command" }),
        })
    );
}

#[tokio::test]
async fn build_post_tool_use_payload_uses_tool_output_wire_value() {
    let payload = ToolPayload::Function {
        arguments: json!({ "command": "printf shell command" }).to_string(),
    };
    let output = FunctionToolOutput {
        body: vec![],
        success: Some(true),
        post_tool_use_response: Some(json!("shell output")),
    };
    let handler = ShellCommandHandler {
        backend: super::ShellCommandBackend::Classic,
    };
    let (session, turn) = make_session_and_context().await;
    let invocation = ToolInvocation {
        session: session.into(),
        turn: turn.into(),
        cancellation_token: tokio_util::sync::CancellationToken::new(),
        tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
        call_id: "call-42".to_string(),
        tool_name: codex_tools::ToolName::plain("shell_command"),
        source: ToolCallSource::Direct,
        payload,
    };
    assert_eq!(
        handler.post_tool_use_payload(&invocation, &output),
        Some(crate::tools::registry::PostToolUsePayload {
            tool_name: HookToolName::bash(),
            tool_use_id: "call-42".to_string(),
            tool_input: json!({ "command": "printf shell command" }),
            tool_response: json!("shell output"),
        })
    );
}
