#![allow(clippy::unwrap_used)]
#![cfg(unix)]

use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::mount_function_call_agent_response;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use core_test_support::zsh_fork::build_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_profile;
use core_test_support::zsh_fork::zsh_fork_runtime;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

fn write_skill_metadata(home: &Path, name: &str, contents: &str) -> Result<()> {
    let metadata_dir = home.join("skills").join(name).join("agents");
    fs::create_dir_all(&metadata_dir)?;
    fs::write(metadata_dir.join("openai.yaml"), contents)?;
    Ok(())
}

fn shell_command_arguments(command: &str) -> Result<String> {
    Ok(serde_json::to_string(&serde_json::json!({
        "command": command,
        "timeout_ms": 500,
    }))?)
}

async fn submit_turn_with_policies(
    test: &TestCodex,
    prompt: &str,
    approval_policy: AskForApproval,
    permission_profile: PermissionProfile,
) -> Result<()> {
    let (sandbox_policy, permission_profile) =
        turn_permission_fields(permission_profile, test.cwd_path());
    test.codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: prompt.to_string(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(test.config.cwd.clone())),
                approval_policy: Some(approval_policy),
                sandbox_policy: Some(sandbox_policy),
                permission_profile,
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: test.session_configured.model.clone(),
                        reasoning_effort: None,
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;
    Ok(())
}

#[cfg(unix)]
fn write_skill_with_shell_script_contents(
    home: &Path,
    name: &str,
    script_name: &str,
    script_contents: &str,
) -> Result<PathBuf> {
    use std::os::unix::fs::PermissionsExt;

    let skill_dir = home.join("skills").join(name);
    let scripts_dir = skill_dir.join("scripts");
    fs::create_dir_all(&scripts_dir)?;
    fs::write(
        skill_dir.join("SKILL.md"),
        format!(
            r#"---
name: {name}
description: {name} skill
---
"#
        ),
    )?;

    let script_path = scripts_dir.join(script_name);
    fs::write(&script_path, script_contents)?;
    let mut permissions = fs::metadata(&script_path)?.permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&script_path, permissions)?;
    Ok(script_path)
}

fn skill_script_command(test: &TestCodex, script_name: &str) -> Result<String> {
    let script_path = fs::canonicalize(
        test.codex_home_path()
            .join("skills/mbolin-test-skill/scripts")
            .join(script_name),
    )?;
    Ok(shlex::try_join([script_path.to_string_lossy().as_ref()])?)
}

async fn wait_for_exec_approval_request(test: &TestCodex) -> Option<ExecApprovalRequestEvent> {
    wait_for_event_match(test.codex.as_ref(), |event| match event {
        EventMsg::ExecApprovalRequest(request) => Some(Some(request.clone())),
        EventMsg::TurnComplete(_) => Some(None),
        _ => None,
    })
    .await
}

async fn wait_for_turn_complete(test: &TestCodex) {
    wait_for_event(test.codex.as_ref(), |event| {
        matches!(event, EventMsg::TurnComplete(_))
    })
    .await;
}

fn output_shows_sandbox_denial(output: &str) -> bool {
    output.contains("Permission denied")
        || output.contains("Operation not permitted")
        || output.contains("Read-only file system")
}

#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<()> {
    skip_if_no_network!(Ok(()));

    let Some(runtime) = zsh_fork_runtime("zsh-fork skill script ignores permissions test")? else {
        return Ok(());
    };

    let approval_policy = AskForApproval::Granular(GranularApprovalConfig {
        sandbox_approval: true,
        rules: true,
        skill_approval: false,
        request_permissions: true,
        mcp_elicitations: true,
    });
    let workspace_write_profile = restrictive_workspace_write_profile();
    let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
    let allowed_dir = outside_dir.path().join("allowed-output");
    fs::create_dir_all(&allowed_dir)?;
    let allowed_path = allowed_dir.join("allowed.txt");
    let allowed_path_quoted = shlex::try_join([allowed_path.to_string_lossy().as_ref()])?;
    let script_contents = format!(
        "#!/bin/sh\nprintf '%s' allowed > {allowed_path_quoted}\nif [ -f {allowed_path_quoted} ]; then cat {allowed_path_quoted}; fi\n"
    );
    let permissions_yaml = format!(
        "permissions:\n  file_system:\n    write:\n      - \"{}\"\n",
        allowed_dir.display()
    );

    let server = start_mock_server().await;
    let allowed_path_for_hook = allowed_path.clone();
    let script_contents_for_hook = script_contents.clone();
    let test = build_zsh_fork_test(
        &server,
        runtime,
        approval_policy,
        workspace_write_profile.clone(),
        move |home| {
            let _ = fs::remove_file(&allowed_path_for_hook);
            write_skill_with_shell_script_contents(
                home,
                "mbolin-test-skill",
                "sandboxed.sh",
                &script_contents_for_hook,
            )
            .unwrap();
            write_skill_metadata(home, "mbolin-test-skill", &permissions_yaml).unwrap();
        },
    )
    .await?;

    let command = skill_script_command(&test, "sandboxed.sh")?;
    let call_id = "zsh-fork-skill-script-ignores-permissions";
    let arguments = shell_command_arguments(&command)?;
    let mocks =
        mount_function_call_agent_response(&server, call_id, &arguments, "shell_command").await;

    submit_turn_with_policies(
        &test,
        "use $mbolin-test-skill",
        approval_policy,
        workspace_write_profile,
    )
    .await?;

    let approval = wait_for_exec_approval_request(&test).await;
    assert!(
        approval.is_none(),
        "expected skill script execution to skip the removed skill approval path"
    );

    wait_for_turn_complete(&test).await;

    let call_output = mocks
        .completion
        .single_request()
        .function_call_output(call_id);
    let output = call_output["output"].as_str().unwrap_or_default();
    assert!(
        !output.contains("Execution denied: Execution forbidden by policy"),
        "skill script should now be governed by the turn sandbox, not the removed skill approval gate: {output:?}"
    );
    assert!(
        output_shows_sandbox_denial(output) || !output.contains("allowed"),
        "expected the turn sandbox to block the out-of-workspace write, got output: {output:?}"
    );
    assert!(
        !allowed_path.exists(),
        "declared skill permissions should not widen script execution beyond the turn sandbox"
    );

    Ok(())
}

#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> {
    skip_if_no_network!(Ok(()));

    let Some(runtime) = zsh_fork_runtime("zsh-fork workspace sandbox test")? else {
        return Ok(());
    };

    let server = start_mock_server().await;
    let tool_call_id = "zsh-fork-workspace-write-deny";
    let outside_path = "/tmp/codex-zsh-fork-workspace-write-deny.txt";
    let workspace_write_profile = restrictive_workspace_write_profile();
    let _ = fs::remove_file(outside_path);
    let test = build_zsh_fork_test(
        &server,
        runtime,
        AskForApproval::Never,
        workspace_write_profile.clone(),
        move |_| {
            let _ = fs::remove_file(outside_path);
        },
    )
    .await?;

    let command = format!("touch {outside_path}");
    let arguments = shell_command_arguments(&command)?;
    let mocks =
        mount_function_call_agent_response(&server, tool_call_id, &arguments, "shell_command")
            .await;

    submit_turn_with_policies(
        &test,
        "write outside workspace with zsh fork",
        AskForApproval::Never,
        workspace_write_profile,
    )
    .await?;

    wait_for_turn_complete(&test).await;

    let call_output = mocks
        .completion
        .single_request()
        .function_call_output(tool_call_id);
    let output = call_output["output"].as_str().unwrap_or_default();
    assert!(
        output_shows_sandbox_denial(output),
        "expected sandbox denial, got output: {output:?}"
    );
    assert!(
        !Path::new(outside_path).exists(),
        "command should not write outside workspace under WorkspaceWrite policy"
    );

    Ok(())
}
