use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;

use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::TestAppServer;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::PluginListParams;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::PluginShareCheckoutResponse;
use codex_app_server_protocol::PluginShareContext;
use codex_app_server_protocol::PluginShareDeleteResponse;
use codex_app_server_protocol::PluginShareDiscoverability;
use codex_app_server_protocol::PluginShareListItem;
use codex_app_server_protocol::PluginShareListResponse;
use codex_app_server_protocol::PluginSharePrincipal;
use codex_app_server_protocol::PluginSharePrincipalRole;
use codex_app_server_protocol::PluginSharePrincipalType;
use codex_app_server_protocol::PluginShareSaveResponse;
use codex_app_server_protocol::PluginShareUpdateTargetsResponse;
use codex_app_server_protocol::PluginSource;
use codex_app_server_protocol::PluginSummary;
use codex_app_server_protocol::RequestId;
use codex_config::types::AuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use flate2::Compression;
use flate2::write::GzEncoder;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_json;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
use wiremock::matchers::query_param;

const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str =
    "CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS";

#[tokio::test]
async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;
    write_corrupt_plugin_share_local_path_mapping(codex_home.path())?;

    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace/upload-url"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(201).set_body_json(json!({
            "file_id": "file_123",
            "upload_url": format!("{}/upload/file_123", server.uri()),
            "etag": "\"upload_etag_123\"",
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/upload/file_123"))
        .and(header("x-ms-blob-type", "BlockBlob"))
        .and(header("content-type", "application/gzip"))
        .respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_123\""))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .and(body_json(json!({
            "file_id": "file_123",
            "etag": "\"upload_etag_123\"",
        })))
        .respond_with(ResponseTemplate::new(201).set_body_json(json!({
            "plugin_id": "plugins_123",
            "share_url": "https://chatgpt.example/plugins/share/share-key-1",
        })))
        .expect(1)
        .mount(&server)
        .await;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let expected_plugin_path = AbsolutePathBuf::try_from(plugin_path.clone())?;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": expected_plugin_path.clone(),
            })),
        )
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareSaveResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareSaveResponse {
            remote_plugin_id: "plugins_123".to_string(),
            share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
        }
    );

    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/workspace/created"))
        .and(query_param("limit", "200"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/installed"))
        .and(query_param("scope", "WORKSPACE"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [installed_remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;

    let request_id = mcp
        .send_raw_request("plugin/share/list", Some(json!({})))
        .await?;
    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareListResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareListResponse {
            data: vec![PluginShareListItem {
                plugin: PluginSummary {
                    id: "demo-plugin@workspace-shared-with-me".to_string(),
                    remote_plugin_id: Some("plugins_123".to_string()),
                    local_version: None,
                    name: "demo-plugin".to_string(),
                    share_context: Some(expected_share_context("plugins_123")),
                    source: PluginSource::Remote,
                    installed: true,
                    enabled: true,
                    install_policy: PluginInstallPolicy::Available,
                    auth_policy: PluginAuthPolicy::OnUse,
                    availability: codex_app_server_protocol::PluginAvailability::Available,
                    interface: Some(expected_plugin_interface()),
                    keywords: Vec::new(),
                },
                local_plugin_path: Some(expected_plugin_path),
            }],
        }
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_save_forwards_access_policy() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace/upload-url"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(201).set_body_json(json!({
            "file_id": "file_123",
            "upload_url": format!("{}/upload/file_123", server.uri()),
            "etag": "\"upload_etag_123\"",
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/upload/file_123"))
        .respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_123\""))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace"))
        .and(body_json(json!({
            "file_id": "file_123",
            "etag": "\"upload_etag_123\"",
            "discoverability": "UNLISTED",
            "share_targets": [
                {
                    "principal_type": "user",
                    "principal_id": "user-1",
                    "role": "editor",
                },
                {
                    "principal_type": "workspace",
                    "principal_id": "account-123",
                    "role": "reader",
                },
            ],
        })))
        .respond_with(ResponseTemplate::new(201).set_body_json(json!({
            "plugin_id": "plugins_123",
            "share_url": "https://chatgpt.example/plugins/share/share-key-1",
        })))
        .expect(1)
        .mount(&server)
        .await;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let expected_plugin_path = AbsolutePathBuf::try_from(plugin_path)?;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": expected_plugin_path,
                "discoverability": "UNLISTED",
                "shareTargets": [
                    {
                        "principalType": "user",
                        "principalId": "user-1",
                        "role": "editor",
                    },
                ],
            })),
        )
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareSaveResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareSaveResponse {
            remote_plugin_id: "plugins_123".to_string(),
            share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
        }
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_save_rejects_listed_discoverability() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
                "discoverability": "LISTED",
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(
        error.error.message,
        "discoverability LISTED is not supported for plugin/share/save; use UNLISTED or PRIVATE"
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_save_rejects_when_plugin_sharing_disabled() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    std::fs::write(
        codex_home.path().join("config.toml"),
        format!(
            r#"
chatgpt_base_url = "{}/backend-api"

[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
            server.uri()
        ),
    )?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(error.error.message, "plugin sharing is disabled");
    assert!(
        server
            .received_requests()
            .await
            .expect("wiremock should record requests")
            .is_empty()
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
                "discoverability": "UNLISTED",
                "shareTargets": [
                    {
                        "principalType": "workspace",
                        "principalId": "account-123",
                        "role": "reader",
                    },
                ],
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(
        error.error.message,
        "shareTargets cannot include workspace principals; use discoverability UNLISTED for workspace link access"
    );

    let request_id = mcp
        .send_raw_request(
            "plugin/share/updateTargets",
            Some(json!({
                "remotePluginId": "plugins_123",
                "discoverability": "UNLISTED",
                "shareTargets": [
                    {
                        "principalType": "workspace",
                        "principalId": "account-123",
                        "role": "reader",
                    },
                ],
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(
        error.error.message,
        "shareTargets cannot include workspace principals; use discoverability UNLISTED for workspace link access"
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result<()> {
    let codex_home = TempDir::new()?;
    let plugin_root = TempDir::new()?;
    let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/save",
            Some(json!({
                "pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
                "remotePluginId": "plugins_123",
                "discoverability": "PRIVATE",
                "shareTargets": [
                    {
                        "principalType": "user",
                        "principalId": "user-1",
                        "role": "reader",
                    },
                ],
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(
        error.error.message,
        "discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share settings"
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
    let codex_home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/workspace/created"))
        .and(query_param("limit", "200"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/installed"))
        .and(query_param("scope", "WORKSPACE"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [installed_remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request("plugin/share/list", Some(json!({})))
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareListResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareListResponse {
            data: vec![PluginShareListItem {
                plugin: PluginSummary {
                    id: "demo-plugin@workspace-shared-with-me".to_string(),
                    remote_plugin_id: Some("plugins_123".to_string()),
                    local_version: None,
                    name: "demo-plugin".to_string(),
                    share_context: Some(expected_share_context("plugins_123")),
                    source: PluginSource::Remote,
                    installed: true,
                    enabled: true,
                    install_policy: PluginInstallPolicy::Available,
                    auth_policy: PluginAuthPolicy::OnUse,
                    availability: codex_app_server_protocol::PluginAvailability::Available,
                    interface: Some(expected_plugin_interface()),
                    keywords: Vec::new(),
                },
                local_plugin_path: None,
            }],
        }
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_checkout_adds_personal_marketplace_entry() -> Result<()> {
    let codex_home = TempDir::new()?;
    let home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let bundle_url = mount_remote_plugin_bundle(
        &server,
        "demo-plugin",
        remote_plugin_bundle_tar_gz_bytes("demo-plugin")?,
    )
    .await;
    mount_remote_plugin_detail_with_bundle(
        &server,
        "plugins_123",
        "demo-plugin",
        &bundle_url,
        "WORKSPACE",
    )
    .await;
    mount_empty_remote_installed_plugins(&server, "WORKSPACE").await;

    let home_env = home.path().to_string_lossy().into_owned();
    let mut mcp = TestAppServer::new_with_env(
        codex_home.path(),
        &[
            ("HOME", Some(home_env.as_str())),
            ("USERPROFILE", Some(home_env.as_str())),
            (TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1")),
        ],
    )
    .await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_raw_request(
            "plugin/share/checkout",
            Some(json!({
                "remotePluginId": "plugins_123",
            })),
        )
        .await?;
    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareCheckoutResponse = to_response(response)?;

    let plugin_path = AbsolutePathBuf::try_from(home.path().join("plugins/demo-plugin"))?;
    let marketplace_path =
        AbsolutePathBuf::try_from(home.path().join(".agents/plugins/marketplace.json"))?;
    assert_eq!(
        response,
        PluginShareCheckoutResponse {
            remote_plugin_id: "plugins_123".to_string(),
            plugin_id: "demo-plugin@codex-curated".to_string(),
            plugin_name: "demo-plugin".to_string(),
            plugin_path: plugin_path.clone(),
            marketplace_name: "codex-curated".to_string(),
            marketplace_path: marketplace_path.clone(),
            remote_version: Some("1.2.3".to_string()),
        }
    );
    assert!(
        plugin_path
            .as_path()
            .join(".codex-plugin/plugin.json")
            .is_file()
    );

    let marketplace: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(marketplace_path.as_path())?)?;
    assert_eq!(
        marketplace,
        json!({
            "name": "codex-curated",
            "interface": {
                "displayName": "Personal",
            },
            "plugins": [
                {
                    "name": "demo-plugin",
                    "source": {
                        "source": "local",
                        "path": "./plugins/demo-plugin",
                    },
                    "policy": {
                        "installation": "AVAILABLE",
                        "authentication": "ON_USE",
                    },
                },
            ],
        })
    );

    let mapping: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(
        codex_home
            .path()
            .join(".tmp/plugin-share-local-paths-v1.json"),
    )?)?;
    assert_eq!(
        mapping,
        json!({
            "localPluginPathsByRemotePluginId": {
                "plugins_123": plugin_path.clone(),
            },
        })
    );

    let request_id = mcp
        .send_plugin_list_request(PluginListParams {
            cwds: None,
            marketplace_kinds: Some(vec![
                codex_app_server_protocol::PluginListMarketplaceKind::Local,
            ]),
        })
        .await?;
    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginListResponse = to_response(response)?;
    assert_eq!(response.marketplaces.len(), 1);
    assert_eq!(response.marketplaces[0].name, "codex-curated");
    assert_eq!(response.marketplaces[0].plugins[0].name, "demo-plugin");
    assert_eq!(
        response.marketplaces[0].plugins[0]
            .share_context
            .as_ref()
            .map(|context| context.remote_plugin_id.as_str()),
        Some("plugins_123")
    );

    std::fs::write(plugin_path.as_path().join("local-edit.txt"), "keep")?;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/checkout",
            Some(json!({
                "remotePluginId": "plugins_123",
            })),
        )
        .await?;
    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareCheckoutResponse = to_response(response)?;
    assert_eq!(response.plugin_path, plugin_path);
    assert_eq!(
        std::fs::read_to_string(plugin_path.as_path().join("local-edit.txt"))?,
        "keep"
    );

    Ok(())
}

#[tokio::test]
async fn plugin_share_checkout_rejects_non_share_remote_plugin() -> Result<()> {
    let codex_home = TempDir::new()?;
    let home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let bundle_url = format!("{}/bundles/global-plugin.tar.gz", server.uri());
    mount_remote_plugin_detail_with_bundle(
        &server,
        "plugins_global",
        "global-plugin",
        &bundle_url,
        "GLOBAL",
    )
    .await;
    mount_empty_remote_installed_plugins(&server, "GLOBAL").await;

    let home_env = home.path().to_string_lossy().into_owned();
    let mut mcp = TestAppServer::new_with_env(
        codex_home.path(),
        &[
            ("HOME", Some(home_env.as_str())),
            ("USERPROFILE", Some(home_env.as_str())),
            (TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1")),
        ],
    )
    .await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_raw_request(
            "plugin/share/checkout",
            Some(json!({
                "remotePluginId": "plugins_global",
            })),
        )
        .await?;
    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert!(
        error
            .error
            .message
            .contains("not available for plugin/share/checkout")
    );
    assert!(!home.path().join("plugins/global-plugin").exists());

    Ok(())
}

#[tokio::test]
async fn plugin_share_checkout_cleans_up_path_when_marketplace_update_fails() -> Result<()> {
    let codex_home = TempDir::new()?;
    let home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let marketplace_path = home.path().join(".agents/plugins/marketplace.json");
    std::fs::create_dir_all(
        marketplace_path
            .parent()
            .expect("marketplace path has parent"),
    )?;
    std::fs::write(
        &marketplace_path,
        serde_json::to_string_pretty(&json!({
            "name": "codex-curated",
            "plugins": [
                {
                    "name": "demo-plugin",
                    "source": {
                        "source": "local",
                        "path": "./other/demo-plugin",
                    },
                },
            ],
        }))?,
    )?;

    let bundle_url = mount_remote_plugin_bundle(
        &server,
        "demo-plugin",
        remote_plugin_bundle_tar_gz_bytes("demo-plugin")?,
    )
    .await;
    mount_remote_plugin_detail_with_bundle(
        &server,
        "plugins_123",
        "demo-plugin",
        &bundle_url,
        "WORKSPACE",
    )
    .await;
    mount_empty_remote_installed_plugins(&server, "WORKSPACE").await;

    let home_env = home.path().to_string_lossy().into_owned();
    let mut mcp = TestAppServer::new_with_env(
        codex_home.path(),
        &[
            ("HOME", Some(home_env.as_str())),
            ("USERPROFILE", Some(home_env.as_str())),
            (TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1")),
        ],
    )
    .await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_raw_request(
            "plugin/share/checkout",
            Some(json!({
                "remotePluginId": "plugins_123",
            })),
        )
        .await?;
    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert!(
        error
            .error
            .message
            .contains("marketplace already contains plugin `demo-plugin`")
    );
    assert!(!home.path().join("plugins/demo-plugin").exists());
    assert!(
        !codex_home
            .path()
            .join(".tmp/plugin-share-local-paths-v1.json")
            .exists()
    );

    Ok(())
}

#[tokio::test]
async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
    let codex_home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    Mock::given(method("PUT"))
        .and(path("/backend-api/ps/plugins/plugins_123/shares"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .and(body_json(json!({
            "discoverability": "UNLISTED",
            "targets": [
                {
                    "principal_type": "user",
                    "principal_id": "user-1",
                    "role": "editor",
                },
                {
                    "principal_type": "workspace",
                    "principal_id": "account-123",
                    "role": "reader",
                },
            ],
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "principals": [
                {
                    "principal_type": "user",
                    "principal_id": "owner-1",
                    "role": "owner",
                    "name": "Owner",
                },
                {
                    "principal_type": "user",
                    "principal_id": "user-1",
                    "role": "editor",
                    "name": "Gavin",
                },
                {
                    "principal_type": "workspace",
                    "principal_id": "account-123",
                    "role": "reader",
                    "name": "Workspace",
                },
            ],
            "discoverability": "UNLISTED",
        })))
        .expect(1)
        .mount(&server)
        .await;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/updateTargets",
            Some(json!({
                "remotePluginId": "plugins_123",
                "discoverability": "UNLISTED",
                "shareTargets": [
                    {
                        "principalType": "user",
                        "principalId": "user-1",
                        "role": "editor",
                    },
                ],
            })),
        )
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareUpdateTargetsResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareUpdateTargetsResponse {
            principals: vec![
                PluginSharePrincipal {
                    principal_type: PluginSharePrincipalType::User,
                    principal_id: "owner-1".to_string(),
                    role: PluginSharePrincipalRole::Owner,
                    name: "Owner".to_string(),
                },
                PluginSharePrincipal {
                    principal_type: PluginSharePrincipalType::User,
                    principal_id: "user-1".to_string(),
                    role: PluginSharePrincipalRole::Editor,
                    name: "Gavin".to_string(),
                },
                PluginSharePrincipal {
                    principal_type: PluginSharePrincipalType::Workspace,
                    principal_id: "account-123".to_string(),
                    role: PluginSharePrincipalRole::Reader,
                    name: "Workspace".to_string(),
                },
            ],
            discoverability: codex_app_server_protocol::PluginShareDiscoverability::Unlisted,
        }
    );
    Ok(())
}

#[tokio::test]
async fn plugin_share_update_targets_rejects_when_plugin_sharing_disabled() -> Result<()> {
    let codex_home = TempDir::new()?;
    let server = MockServer::start().await;
    std::fs::write(
        codex_home.path().join("config.toml"),
        format!(
            r#"
chatgpt_base_url = "{}/backend-api"

[features]
plugins = true
remote_plugin = true
plugin_sharing = false
"#,
            server.uri()
        ),
    )?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/updateTargets",
            Some(json!({
                "remotePluginId": "plugins_123",
                "discoverability": "UNLISTED",
                "shareTargets": [],
            })),
        )
        .await?;

    let error: JSONRPCError = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
    )
    .await??;

    assert_eq!(error.error.code, -32600);
    assert_eq!(error.error.message, "plugin sharing is disabled");
    Ok(())
}

#[tokio::test]
async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
    let codex_home = TempDir::new()?;
    let server = MockServer::start().await;
    write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
    write_chatgpt_auth(
        codex_home.path(),
        ChatGptAuthFixture::new("chatgpt-token")
            .account_id("account-123")
            .chatgpt_user_id("user-123")
            .chatgpt_account_id("account-123"),
        AuthCredentialsStoreMode::File,
    )?;
    let local_plugin_path = AbsolutePathBuf::try_from(codex_home.path().join("local-plugin"))?;
    write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &local_plugin_path)?;

    Mock::given(method("DELETE"))
        .and(path("/backend-api/public/plugins/workspace/plugins_123"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(204))
        .expect(1)
        .mount(&server)
        .await;

    let mut mcp = TestAppServer::new(codex_home.path()).await?;
    timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
    let request_id = mcp
        .send_raw_request(
            "plugin/share/delete",
            Some(json!({
                "remotePluginId": "plugins_123",
            })),
        )
        .await?;

    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareDeleteResponse = to_response(response)?;

    assert_eq!(response, PluginShareDeleteResponse {});

    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/workspace/created"))
        .and(query_param("limit", "200"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/installed"))
        .and(query_param("scope", "WORKSPACE"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [installed_remote_plugin_json("plugins_123")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;

    let request_id = mcp
        .send_raw_request("plugin/share/list", Some(json!({})))
        .await?;
    let response: JSONRPCResponse = timeout(
        DEFAULT_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let response: PluginShareListResponse = to_response(response)?;

    assert_eq!(
        response,
        PluginShareListResponse {
            data: vec![PluginShareListItem {
                plugin: PluginSummary {
                    id: "demo-plugin@workspace-shared-with-me".to_string(),
                    remote_plugin_id: Some("plugins_123".to_string()),
                    local_version: None,
                    name: "demo-plugin".to_string(),
                    share_context: Some(expected_share_context("plugins_123")),
                    source: PluginSource::Remote,
                    installed: true,
                    enabled: true,
                    install_policy: PluginInstallPolicy::Available,
                    auth_policy: PluginAuthPolicy::OnUse,
                    availability: codex_app_server_protocol::PluginAvailability::Available,
                    interface: Some(expected_plugin_interface()),
                    keywords: Vec::new(),
                },
                local_plugin_path: None,
            }],
        }
    );
    Ok(())
}

fn write_remote_plugin_config(codex_home: &Path, base_url: &str) -> std::io::Result<()> {
    std::fs::write(
        codex_home.join("config.toml"),
        format!(
            r#"
chatgpt_base_url = "{base_url}"

[features]
plugins = true
remote_plugin = true
"#
        ),
    )
}

async fn mount_remote_plugin_bundle(
    server: &MockServer,
    plugin_name: &str,
    body: Vec<u8>,
) -> String {
    let bundle_path = format!("/bundles/{plugin_name}.tar.gz");
    Mock::given(method("GET"))
        .and(path(bundle_path.clone()))
        .respond_with(
            ResponseTemplate::new(200)
                .insert_header("content-type", "application/gzip")
                .set_body_bytes(body),
        )
        .expect(1)
        .mount(server)
        .await;
    format!("{}{}", server.uri(), bundle_path)
}

async fn mount_remote_plugin_detail_with_bundle(
    server: &MockServer,
    remote_plugin_id: &str,
    plugin_name: &str,
    bundle_url: &str,
    scope: &str,
) {
    Mock::given(method("GET"))
        .and(path(format!("/backend-api/ps/plugins/{remote_plugin_id}")))
        .and(query_param("includeDownloadUrls", "true"))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "id": remote_plugin_id,
            "name": plugin_name,
            "scope": scope,
            "discoverability": "PRIVATE",
            "share_url": "https://chatgpt.example/plugins/share/share-key-1",
            "share_principals": [
                {
                    "principal_type": "user",
                    "principal_id": "user-owner__account-123",
                    "role": "owner",
                    "name": "Owner",
                },
            ],
            "installation_policy": "AVAILABLE",
            "authentication_policy": "ON_USE",
            "release": {
                "version": "1.2.3",
                "bundle_download_url": bundle_url,
                "display_name": "Demo Plugin",
                "description": "Demo plugin description",
                "interface": {
                    "short_description": "A demo plugin",
                    "capabilities": ["Read", "Write"],
                },
                "skills": [],
            },
        })))
        .mount(server)
        .await;
}

async fn mount_empty_remote_installed_plugins(server: &MockServer, scope: &str) {
    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/installed"))
        .and(query_param("scope", scope))
        .and(header("authorization", "Bearer chatgpt-token"))
        .and(header("chatgpt-account-id", "account-123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [],
            "pagination": {
                "next_page_token": null,
            },
        })))
        .mount(server)
        .await;
}

fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
    json!({
        "id": plugin_id,
        "name": "demo-plugin",
        "scope": "WORKSPACE",
        "discoverability": "PRIVATE",
        "share_url": "https://chatgpt.example/plugins/share/share-key-1",
        "share_principals": [
            {
                "principal_type": "user",
                "principal_id": "user-owner__account-123",
                "role": "owner",
                "name": "Owner"
            },
            {
                "principal_type": "user",
                "principal_id": "user-reader__account-123",
                "role": "reader",
                "name": "Reader"
            }
        ],
        "installation_policy": "AVAILABLE",
        "authentication_policy": "ON_USE",
        "release": {
            "version": "0.1.0",
            "display_name": "Demo Plugin",
            "description": "Demo plugin description",
            "interface": {
                "short_description": "A demo plugin",
                "capabilities": ["Read", "Write"]
            },
            "skills": []
        }
    })
}

fn installed_remote_plugin_json(plugin_id: &str) -> serde_json::Value {
    let mut plugin = remote_plugin_json(plugin_id);
    let serde_json::Value::Object(fields) = &mut plugin else {
        unreachable!("plugin json should be an object");
    };
    fields.insert("enabled".to_string(), json!(true));
    fields.insert("disabled_skill_names".to_string(), json!([]));
    plugin
}

fn empty_pagination_json() -> serde_json::Value {
    json!({
        "next_page_token": null
    })
}

fn expected_plugin_interface() -> PluginInterface {
    PluginInterface {
        display_name: Some("Demo Plugin".to_string()),
        short_description: Some("A demo plugin".to_string()),
        long_description: None,
        developer_name: None,
        category: None,
        capabilities: vec!["Read".to_string(), "Write".to_string()],
        website_url: None,
        privacy_policy_url: None,
        terms_of_service_url: None,
        default_prompt: None,
        brand_color: None,
        composer_icon: None,
        composer_icon_url: None,
        logo: None,
        logo_url: None,
        screenshots: Vec::new(),
        screenshot_urls: Vec::new(),
    }
}

fn expected_share_context(plugin_id: &str) -> PluginShareContext {
    PluginShareContext {
        remote_plugin_id: plugin_id.to_string(),
        remote_version: Some("0.1.0".to_string()),
        discoverability: Some(PluginShareDiscoverability::Private),
        share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
        creator_account_user_id: None,
        creator_name: None,
        share_principals: Some(vec![
            PluginSharePrincipal {
                principal_type: PluginSharePrincipalType::User,
                principal_id: "user-owner__account-123".to_string(),
                role: PluginSharePrincipalRole::Owner,
                name: "Owner".to_string(),
            },
            PluginSharePrincipal {
                principal_type: PluginSharePrincipalType::User,
                principal_id: "user-reader__account-123".to_string(),
                role: PluginSharePrincipalRole::Reader,
                name: "Reader".to_string(),
            },
        ]),
    }
}

fn write_test_plugin(root: &Path, plugin_name: &str) -> std::io::Result<PathBuf> {
    let plugin_path = root.join(plugin_name);
    write_file(
        &plugin_path.join(".codex-plugin/plugin.json"),
        &format!(r#"{{"name":"{plugin_name}"}}"#),
    )?;
    write_file(
        &plugin_path.join("skills/example/SKILL.md"),
        "# Example\n\nA test skill.\n",
    )?;
    Ok(plugin_path)
}

fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>> {
    let manifest = format!(r#"{{"name":"{plugin_name}"}}"#);
    let skill = "# Example\n\nA test skill.\n";
    let encoder = GzEncoder::new(Vec::new(), Compression::default());
    let mut tar = tar::Builder::new(encoder);
    for (path, contents, mode) in [
        (
            ".codex-plugin/plugin.json",
            manifest.as_bytes(),
            /*mode*/ 0o644,
        ),
        (
            "skills/example/SKILL.md",
            skill.as_bytes(),
            /*mode*/ 0o644,
        ),
    ] {
        let mut header = tar::Header::new_gnu();
        header.set_size(contents.len() as u64);
        header.set_mode(mode);
        header.set_cksum();
        tar.append_data(&mut header, path, contents)?;
    }
    Ok(tar.into_inner()?.finish()?)
}

fn write_corrupt_plugin_share_local_path_mapping(codex_home: &Path) -> std::io::Result<()> {
    write_file(
        &codex_home.join(".tmp/plugin-share-local-paths-v1.json"),
        "not-json",
    )
}

fn write_plugin_share_local_path_mapping(
    codex_home: &Path,
    remote_plugin_id: &str,
    plugin_path: &AbsolutePathBuf,
) -> std::io::Result<()> {
    let mut local_plugin_paths_by_remote_plugin_id = serde_json::Map::new();
    local_plugin_paths_by_remote_plugin_id.insert(
        remote_plugin_id.to_string(),
        serde_json::to_value(plugin_path).map_err(std::io::Error::other)?,
    );
    let contents = serde_json::to_string_pretty(&json!({
        "localPluginPathsByRemotePluginId": local_plugin_paths_by_remote_plugin_id,
    }))
    .map_err(std::io::Error::other)?;
    write_file(
        &codex_home.join(".tmp/plugin-share-local-paths-v1.json"),
        &format!("{contents}\n"),
    )
}

fn write_file(path: &Path, contents: &str) -> std::io::Result<()> {
    let Some(parent) = path.parent() else {
        return Err(std::io::Error::other(format!(
            "file path `{}` should have a parent",
            path.display()
        )));
    };
    std::fs::create_dir_all(parent)?;
    std::fs::write(path, contents)
}
