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

use codex_protocol::models::ShellCommandToolCallParams;
use pretty_assertions::assert_eq;

use crate::codex::make_session_and_context;
use crate::exec_env::create_env;
use crate::is_safe_command::is_known_safe_command;
use crate::powershell::try_find_powershell_executable_blocking;
use crate::powershell::try_find_pwsh_executable_blocking;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::shell_snapshot::ShellSnapshot;
use crate::tools::handlers::ShellCommandHandler;
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 */ true)
    ));
    assert!(is_known_safe_command(
        &shell.derive_exec_args(command, /* 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, 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,
        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: PathBuf::from("/tmp/snapshot.sh"),
        cwd: PathBuf::from("/tmp"),
    })));
    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", true);
    assert_eq!(
        login_command,
        shell.derive_exec_args("echo login shell", true)
    );

    let non_login_command =
        ShellCommandHandler::base_command(&shell, "echo non login shell", false);
    assert_eq!(
        non_login_command,
        shell.derive_exec_args("echo non 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,
        false,
    )
    .expect("non-login shells should still be allowed");

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

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

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