use super::*;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInterface;
use codex_login::CodexAuth;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use std::fs;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
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;
use wiremock::matchers::query_param_is_missing;

fn test_config(server: &MockServer) -> RemotePluginServiceConfig {
    RemotePluginServiceConfig {
        chatgpt_base_url: format!("{}/backend-api", server.uri()),
    }
}

fn test_auth() -> CodexAuth {
    CodexAuth::create_dummy_chatgpt_auth_for_testing()
}

fn write_file(path: &Path, contents: &str) {
    fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap();
    fs::write(path, contents).unwrap();
}

fn write_test_plugin(root: &Path, plugin_name: &str) -> 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",
    );
    plugin_path
}

fn write_plugin_share_local_path_mapping(
    codex_home: &Path,
    remote_plugin_id: &str,
    plugin_path: &AbsolutePathBuf,
) {
    write_file(
        &codex_home.join(".tmp/plugin-share-local-paths-v1.json"),
        &format!(
            "{}\n",
            serde_json::to_string_pretty(&json!({
                "localPluginPathsByRemotePluginId": {
                    remote_plugin_id: plugin_path,
                },
            }))
            .unwrap()
        ),
    );
}

fn archive_file_entries(archive_bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
    let decoder = flate2::read::GzDecoder::new(archive_bytes);
    let mut archive = tar::Archive::new(decoder);
    archive
        .entries()
        .unwrap()
        .filter_map(|entry| {
            let mut entry = entry.unwrap();
            if !entry.header().entry_type().is_file() {
                return None;
            }
            let path = entry.path().unwrap().to_string_lossy().into_owned();
            let mut contents = Vec::new();
            entry.read_to_end(&mut contents).unwrap();
            Some((path, contents))
        })
        .collect()
}

fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
    json!({
        "id": plugin_id,
        "name": "demo-plugin",
        "scope": "WORKSPACE",
        "discoverability": "PRIVATE",
        "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 remote_plugin_json_with_share_url_and_principals(
    plugin_id: &str,
    share_url: Option<&str>,
    share_principals: serde_json::Value,
) -> 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("discoverability".to_string(), json!("PRIVATE"));
    fields.insert("share_url".to_string(), json!(share_url));
    fields.insert("share_principals".to_string(), share_principals);
    plugin
}

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(),
    }
}

#[tokio::test]
async fn save_remote_plugin_share_creates_workspace_plugin() {
    let codex_home = TempDir::new().unwrap();
    let temp_dir = TempDir::new().unwrap();
    let plugin_path =
        AbsolutePathBuf::try_from(write_test_plugin(temp_dir.path(), "demo-plugin")).unwrap();
    let archive_size = archive_plugin_for_upload(plugin_path.as_path())
        .unwrap()
        .len();
    let server = MockServer::start().await;
    let config = test_config(&server);
    let auth = test_auth();

    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace/upload-url"))
        .and(header("authorization", "Bearer Access Token"))
        .and(header("chatgpt-account-id", "account_id"))
        .and(body_json(json!({
            "filename": "demo-plugin.tar.gz",
            "mime_type": "application/gzip",
            "size_bytes": archive_size,
        })))
        .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 Access Token"))
        .and(header("chatgpt-account-id", "account_id"))
        .and(body_json(json!({
            "file_id": "file_123",
            "etag": "\"upload_etag_123\"",
            "discoverability": "UNLISTED",
            "share_targets": [
                {
                    "principal_type": "user",
                    "principal_id": "user-1",
                    "role": "reader",
                },
                {
                    "principal_type": "workspace",
                    "principal_id": "account_id",
                    "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 result = save_remote_plugin_share(
        &config,
        Some(&auth),
        codex_home.path(),
        &plugin_path,
        /*remote_plugin_id*/ None,
        RemotePluginShareAccessPolicy {
            discoverability: Some(RemotePluginShareDiscoverability::Unlisted),
            share_targets: Some(vec![RemotePluginShareTarget {
                principal_type: RemotePluginSharePrincipalType::User,
                principal_id: "user-1".to_string(),
                role: RemotePluginShareTargetRole::Reader,
            }]),
        },
    )
    .await
    .unwrap();

    assert_eq!(
        result,
        RemotePluginShareSaveResult {
            remote_plugin_id: "plugins_123".to_string(),
            share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
        }
    );
    assert_eq!(
        local_paths::load_plugin_share_local_paths(codex_home.path()).unwrap(),
        BTreeMap::from([("plugins_123".to_string(), plugin_path)])
    );

    let requests = server.received_requests().await.unwrap_or_default();
    let upload_request = requests
        .iter()
        .find(|request| request.method == "PUT" && request.url.path() == "/upload/file_123")
        .unwrap();
    let archive_files = archive_file_entries(&upload_request.body);
    assert_eq!(
        archive_files
            .get(".codex-plugin/plugin.json")
            .map(Vec::as_slice),
        Some(br#"{"name":"demo-plugin"}"#.as_slice())
    );
    assert_eq!(
        archive_files
            .get("skills/example/SKILL.md")
            .map(Vec::as_slice),
        Some(b"# Example\n\nA test skill.\n".as_slice())
    );
}

#[test]
fn archive_plugin_for_upload_rejects_archives_over_limit() {
    let temp_dir = TempDir::new().unwrap();
    let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin");
    write_file(
        &plugin_path.join("large.txt"),
        &"0123456789abcdef".repeat(1024),
    );

    let err = archive_plugin_for_upload_with_limit(&plugin_path, /*max_bytes*/ 16)
        .expect_err("oversized plugin archive should fail");

    assert!(matches!(
        err,
        RemotePluginCatalogError::ArchiveTooLarge { .. }
    ));
}

#[test]
fn archive_plugin_for_upload_places_manifest_at_archive_root() {
    let temp_dir = TempDir::new().unwrap();
    let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin");

    let archive_bytes = archive_plugin_for_upload(&plugin_path).unwrap();
    let archive_files = archive_file_entries(&archive_bytes);

    assert_eq!(
        archive_files.keys().cloned().collect::<Vec<_>>(),
        vec![
            ".codex-plugin/plugin.json".to_string(),
            "skills/example/SKILL.md".to_string()
        ]
    );
    assert_eq!(
        archive_files
            .get(".codex-plugin/plugin.json")
            .map(Vec::as_slice),
        Some(br#"{"name":"demo-plugin"}"#.as_slice())
    );
    assert_eq!(
        archive_files
            .get("skills/example/SKILL.md")
            .map(Vec::as_slice),
        Some(b"# Example\n\nA test skill.\n".as_slice())
    );
}

#[test]
fn archive_plugin_for_upload_round_trips_through_plugin_bundle_archive_with_long_paths() {
    let temp_dir = TempDir::new().unwrap();
    let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin");
    let long_skill_path = Path::new("skills")
        .join(["segment"; 40].join("/"))
        .join("SKILL.md");
    write_file(&plugin_path.join(&long_skill_path), "# Long path skill\n");

    let archive_bytes = archive_plugin_for_upload(&plugin_path).unwrap();
    let destination = TempDir::new().unwrap();
    crate::plugin_bundle_archive::unpack_plugin_bundle_tar_gz(
        &archive_bytes,
        destination.path(),
        /*max_total_bytes*/ 1024 * 1024,
    )
    .expect("extract shared plugin archive");

    assert_eq!(
        fs::read_to_string(destination.path().join(".codex-plugin/plugin.json")).unwrap(),
        r#"{"name":"demo-plugin"}"#
    );
    assert_eq!(
        fs::read_to_string(destination.path().join(long_skill_path)).unwrap(),
        "# Long path skill\n"
    );
}

#[tokio::test]
async fn save_remote_plugin_share_updates_existing_workspace_plugin() {
    let codex_home = TempDir::new().unwrap();
    let temp_dir = TempDir::new().unwrap();
    let plugin_path =
        AbsolutePathBuf::try_from(write_test_plugin(temp_dir.path(), "demo-plugin")).unwrap();
    let archive_size = archive_plugin_for_upload(plugin_path.as_path())
        .unwrap()
        .len();
    let server = MockServer::start().await;
    let config = test_config(&server);
    let auth = test_auth();

    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace/upload-url"))
        .and(body_json(json!({
            "filename": "demo-plugin.tar.gz",
            "mime_type": "application/gzip",
            "size_bytes": archive_size,
            "plugin_id": "plugins_123",
        })))
        .respond_with(ResponseTemplate::new(201).set_body_json(json!({
            "file_id": "file_456",
            "upload_url": format!("{}/upload/file_456", server.uri()),
            "etag": "\"upload_etag_456\"",
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/upload/file_456"))
        .respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_456\""))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(path("/backend-api/public/plugins/workspace/plugins_123"))
        .and(body_json(json!({
            "file_id": "file_456",
            "etag": "\"upload_etag_456\"",
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugin_id": "plugins_123",
        })))
        .expect(1)
        .mount(&server)
        .await;

    let result = save_remote_plugin_share(
        &config,
        Some(&auth),
        codex_home.path(),
        &plugin_path,
        Some("plugins_123"),
        RemotePluginShareAccessPolicy::default(),
    )
    .await
    .unwrap();

    assert_eq!(
        result,
        RemotePluginShareSaveResult {
            remote_plugin_id: "plugins_123".to_string(),
            share_url: None,
        }
    );
}

#[tokio::test]
async fn update_remote_plugin_share_targets_updates_targets() {
    let server = MockServer::start().await;
    let config = test_config(&server);
    let auth = test_auth();

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

    let result = update_remote_plugin_share_targets(
        &config,
        Some(&auth),
        "plugins_123",
        vec![
            RemotePluginShareTarget {
                principal_type: RemotePluginSharePrincipalType::User,
                principal_id: "user-1".to_string(),
                role: RemotePluginShareTargetRole::Editor,
            },
            RemotePluginShareTarget {
                principal_type: RemotePluginSharePrincipalType::Group,
                principal_id: "group-1".to_string(),
                role: RemotePluginShareTargetRole::Reader,
            },
        ],
        RemotePluginShareUpdateDiscoverability::Unlisted,
    )
    .await
    .unwrap();

    assert_eq!(
        result,
        RemotePluginShareUpdateTargetsResult {
            principals: vec![
                RemotePluginSharePrincipal {
                    principal_type: RemotePluginSharePrincipalType::User,
                    principal_id: "user-1".to_string(),
                    role: RemotePluginSharePrincipalRole::Editor,
                    name: "Gavin".to_string(),
                },
                RemotePluginSharePrincipal {
                    principal_type: RemotePluginSharePrincipalType::Group,
                    principal_id: "group-1".to_string(),
                    role: RemotePluginSharePrincipalRole::Reader,
                    name: "Engineering".to_string(),
                },
            ],
            discoverability: RemotePluginShareDiscoverability::Unlisted,
        }
    );
}

#[tokio::test]
async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
    let codex_home = TempDir::new().unwrap();
    let local_plugin_path =
        AbsolutePathBuf::try_from(codex_home.path().join("local-plugin")).unwrap();
    write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &local_plugin_path);
    let server = MockServer::start().await;
    let config = test_config(&server);
    let auth = test_auth();

    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/workspace/created"))
        .and(header("authorization", "Bearer Access Token"))
        .and(header("chatgpt-account-id", "account_id"))
        .and(query_param(
            "limit",
            REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string(),
        ))
        .and(query_param_is_missing("pageToken"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [remote_plugin_json_with_share_url_and_principals(
                "plugins_123",
                Some("https://chatgpt.example/plugins/share/share-key-1"),
                json!([
                    {
                        "principal_type": "user",
                        "principal_id": "user-owner",
                        "role": "owner",
                        "name": "Owner",
                    },
                    {
                        "principal_type": "user",
                        "principal_id": "user-reader",
                        "role": "reader",
                        "name": "Reader",
                    },
                ]),
            )],
            "pagination": {
                "next_page_token": "page-2"
            },
        })))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/backend-api/ps/plugins/workspace/created"))
        .and(header("authorization", "Bearer Access Token"))
        .and(header("chatgpt-account-id", "account_id"))
        .and(query_param(
            "limit",
            REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string(),
        ))
        .and(query_param("pageToken", "page-2"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [remote_plugin_json_with_share_url_and_principals(
                "plugins_456",
                /*share_url*/ None,
                json!([
                    {
                        "principal_type": "user",
                        "principal_id": "user-owner",
                        "role": "owner",
                        "name": "Owner",
                    },
                    {
                        "principal_type": "user",
                        "principal_id": "user-editor",
                        "role": "editor",
                        "name": "Editor",
                    },
                ]),
            )],
            "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"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "plugins": [installed_remote_plugin_json("plugins_456")],
            "pagination": empty_pagination_json(),
        })))
        .expect(1)
        .mount(&server)
        .await;

    let result = list_remote_plugin_shares(&config, Some(&auth), codex_home.path())
        .await
        .unwrap();

    assert_eq!(
        result,
        vec![
            RemotePluginShareSummary {
                summary: RemotePluginSummary {
                    id: "demo-plugin@workspace-shared-with-me".to_string(),
                    remote_plugin_id: "plugins_123".to_string(),
                    name: "demo-plugin".to_string(),
                    share_context: Some(RemotePluginShareContext {
                        remote_plugin_id: "plugins_123".to_string(),
                        remote_version: Some("0.1.0".to_string()),
                        discoverability: RemotePluginShareDiscoverability::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![
                            RemotePluginSharePrincipal {
                                principal_type: RemotePluginSharePrincipalType::User,
                                principal_id: "user-owner".to_string(),
                                role: RemotePluginSharePrincipalRole::Owner,
                                name: "Owner".to_string(),
                            },
                            RemotePluginSharePrincipal {
                                principal_type: RemotePluginSharePrincipalType::User,
                                principal_id: "user-reader".to_string(),
                                role: RemotePluginSharePrincipalRole::Reader,
                                name: "Reader".to_string(),
                            },
                        ]),
                    }),
                    installed: false,
                    enabled: false,
                    install_policy: PluginInstallPolicy::Available,
                    auth_policy: PluginAuthPolicy::OnUse,
                    availability: PluginAvailability::Available,
                    interface: Some(expected_plugin_interface()),
                    keywords: Vec::new(),
                },
                local_plugin_path: Some(local_plugin_path),
            },
            RemotePluginShareSummary {
                summary: RemotePluginSummary {
                    id: "demo-plugin@workspace-shared-with-me".to_string(),
                    remote_plugin_id: "plugins_456".to_string(),
                    name: "demo-plugin".to_string(),
                    share_context: Some(RemotePluginShareContext {
                        remote_plugin_id: "plugins_456".to_string(),
                        remote_version: Some("0.1.0".to_string()),
                        discoverability: RemotePluginShareDiscoverability::Private,
                        share_url: None,
                        creator_account_user_id: None,
                        creator_name: None,
                        share_principals: Some(vec![
                            RemotePluginSharePrincipal {
                                principal_type: RemotePluginSharePrincipalType::User,
                                principal_id: "user-owner".to_string(),
                                role: RemotePluginSharePrincipalRole::Owner,
                                name: "Owner".to_string(),
                            },
                            RemotePluginSharePrincipal {
                                principal_type: RemotePluginSharePrincipalType::User,
                                principal_id: "user-editor".to_string(),
                                role: RemotePluginSharePrincipalRole::Editor,
                                name: "Editor".to_string(),
                            },
                        ]),
                    }),
                    installed: true,
                    enabled: true,
                    install_policy: PluginInstallPolicy::Available,
                    auth_policy: PluginAuthPolicy::OnUse,
                    availability: PluginAvailability::Available,
                    interface: Some(expected_plugin_interface()),
                    keywords: Vec::new(),
                },
                local_plugin_path: None,
            }
        ]
    );
}

#[tokio::test]
async fn delete_remote_plugin_share_deletes_workspace_plugin() {
    let codex_home = TempDir::new().unwrap();
    let local_plugin_path =
        AbsolutePathBuf::try_from(codex_home.path().join("local-plugin")).unwrap();
    write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &local_plugin_path);
    let server = MockServer::start().await;
    let config = test_config(&server);
    let auth = test_auth();

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

    delete_remote_plugin_share(&config, Some(&auth), codex_home.path(), "plugins_123")
        .await
        .unwrap();
    assert_eq!(
        local_paths::load_plugin_share_local_paths(codex_home.path()).unwrap(),
        BTreeMap::new()
    );
}
