use super::*;
use crate::config::CONFIG_TOML_FILE;
use crate::config::ConfigBuilder;
use codex_config::AppRequirementToml;
use codex_config::AppToolRequirementToml;
use codex_config::AppToolsRequirementsToml;
use codex_config::AppsRequirementsToml;
use codex_config::CloudRequirementsLoader;
use codex_config::ConfigLayerStack;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
use codex_config::types::AppConfig;
use codex_config::types::AppToolConfig;
use codex_config::types::AppToolsConfig;
use codex_config::types::AppsDefaultConfig;
use codex_connectors::merge::plugin_connector_to_app_info;
use codex_connectors::metadata::connector_install_url;
use codex_connectors::metadata::sanitize_name;
use codex_features::Feature;
use codex_login::CodexAuth;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use rmcp::model::Tool;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use tempfile::tempdir;

fn annotations(destructive_hint: Option<bool>, open_world_hint: Option<bool>) -> ToolAnnotations {
    ToolAnnotations {
        destructive_hint,
        idempotent_hint: None,
        open_world_hint,
        read_only_hint: None,
        title: None,
    }
}

fn app(id: &str) -> AppInfo {
    AppInfo {
        id: id.to_string(),
        name: id.to_string(),
        description: None,
        logo_url: None,
        logo_url_dark: None,
        distribution_channel: None,
        install_url: None,
        branding: None,
        app_metadata: None,
        labels: None,
        is_accessible: false,
        is_enabled: true,
        plugin_display_names: Vec::new(),
    }
}

fn plugin_names(names: &[&str]) -> Vec<String> {
    names.iter().map(ToString::to_string).collect()
}

fn test_tool_definition(tool_name: &str) -> Tool {
    Tool {
        name: tool_name.to_string().into(),
        title: None,
        description: None,
        input_schema: Arc::new(JsonObject::default()),
        output_schema: None,
        annotations: None,
        execution: None,
        icons: None,
        meta: None,
    }
}

fn codex_app_tool(
    tool_name: &str,
    connector_id: &str,
    connector_name: Option<&str>,
    plugin_display_names: &[&str],
) -> ToolInfo {
    let tool_namespace = connector_name
        .map(sanitize_name)
        .map(|connector_name| format!("mcp__{CODEX_APPS_MCP_SERVER_NAME}__{connector_name}"))
        .unwrap_or_else(|| CODEX_APPS_MCP_SERVER_NAME.to_string());

    ToolInfo {
        server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
        supports_parallel_tool_calls: false,
        server_origin: None,
        callable_name: tool_name.to_string(),
        callable_namespace: tool_namespace,
        namespace_description: None,
        tool: test_tool_definition(tool_name),
        connector_id: Some(connector_id.to_string()),
        connector_name: connector_name.map(ToOwned::to_owned),
        plugin_display_names: plugin_names(plugin_display_names),
    }
}

fn with_accessible_connectors_cache_cleared<R>(f: impl FnOnce() -> R) -> R {
    let previous = {
        let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        cache_guard.take()
    };
    let result = f();
    let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    *cache_guard = previous;
    result
}

#[test]
fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
    let tools = vec![
        codex_app_tool(
            "calendar_list_events",
            "calendar",
            /*connector_name*/ None,
            &["sample", "sample"],
        ),
        codex_app_tool(
            "calendar_create_event",
            "calendar",
            Some("Google Calendar"),
            &["beta", "sample"],
        ),
        ToolInfo {
            server_name: "sample".to_string(),
            supports_parallel_tool_calls: false,
            server_origin: None,
            callable_name: "echo".to_string(),
            callable_namespace: "sample".to_string(),
            namespace_description: None,
            tool: test_tool_definition("echo"),
            connector_id: None,
            connector_name: None,
            plugin_display_names: plugin_names(&["ignored"]),
        },
    ];

    let connectors = accessible_connectors_from_mcp_tools(&tools);

    assert_eq!(
        connectors,
        vec![AppInfo {
            id: "calendar".to_string(),
            name: "Google Calendar".to_string(),
            description: None,
            logo_url: None,
            logo_url_dark: None,
            distribution_channel: None,
            install_url: Some(connector_install_url("Google Calendar", "calendar")),
            branding: None,
            app_metadata: None,
            labels: None,
            is_accessible: true,
            is_enabled: true,
            plugin_display_names: plugin_names(&["beta", "sample"]),
        }]
    );
}

#[tokio::test]
async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_installed_apps() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .build()
        .await
        .expect("config should load");
    let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);
    let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
    let tools = vec![
        codex_app_tool(
            "calendar_list_events",
            "calendar",
            Some("Google Calendar"),
            &["calendar-plugin"],
        ),
        codex_app_tool(
            "openai_hidden",
            "connector_openai_hidden",
            Some("Hidden"),
            &[],
        ),
    ];

    let cached = with_accessible_connectors_cache_cleared(|| {
        refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools);
        read_cached_accessible_connectors(&cache_key).expect("cache should be populated")
    });

    assert_eq!(
        cached,
        vec![
            AppInfo {
                id: "calendar".to_string(),
                name: "Google Calendar".to_string(),
                description: None,
                logo_url: None,
                logo_url_dark: None,
                distribution_channel: None,
                install_url: Some(connector_install_url("Google Calendar", "calendar")),
                branding: None,
                app_metadata: None,
                labels: None,
                is_accessible: true,
                is_enabled: true,
                plugin_display_names: plugin_names(&["calendar-plugin"]),
            },
            AppInfo {
                id: "connector_openai_hidden".to_string(),
                name: "Hidden".to_string(),
                description: None,
                logo_url: None,
                logo_url_dark: None,
                distribution_channel: None,
                install_url: Some(connector_install_url("Hidden", "connector_openai_hidden")),
                branding: None,
                app_metadata: None,
                labels: None,
                is_accessible: true,
                is_enabled: true,
                plugin_display_names: Vec::new(),
            }
        ]
    );
}

#[test]
fn accessible_connectors_from_mcp_tools_preserves_description() {
    let mcp_tools = vec![ToolInfo {
        server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
        supports_parallel_tool_calls: false,
        server_origin: None,
        callable_name: "calendar_create_event".to_string(),
        callable_namespace: "mcp__codex_apps__calendar".to_string(),
        namespace_description: Some("Plan events".to_string()),
        tool: Tool {
            name: "calendar_create_event".to_string().into(),
            title: None,
            description: Some("Create a calendar event".into()),
            input_schema: Arc::new(JsonObject::default()),
            output_schema: None,
            annotations: None,
            execution: None,
            icons: None,
            meta: None,
        },
        connector_id: Some("calendar".to_string()),
        connector_name: Some("Calendar".to_string()),
        plugin_display_names: Vec::new(),
    }];

    assert_eq!(
        accessible_connectors_from_mcp_tools(&mcp_tools),
        vec![AppInfo {
            id: "calendar".to_string(),
            name: "Calendar".to_string(),
            description: Some("Plan events".to_string()),
            logo_url: None,
            logo_url_dark: None,
            distribution_channel: None,
            branding: None,
            app_metadata: None,
            labels: None,
            install_url: Some(connector_install_url("Calendar", "calendar")),
            is_accessible: true,
            is_enabled: true,
            plugin_display_names: Vec::new(),
        }]
    );
}

#[test]
fn app_tool_policy_uses_global_defaults_for_destructive_hints() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: true,
            destructive_enabled: false,
            open_world_enabled: true,
        }),
        apps: HashMap::new(),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/create",
        /*tool_title*/ None,
        Some(&annotations(Some(true), /*open_world_hint*/ None)),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_defaults_missing_destructive_hint_to_true() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: true,
            destructive_enabled: false,
            open_world_enabled: true,
        }),
        apps: HashMap::new(),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/create",
        /*tool_title*/ None,
        Some(&annotations(/*destructive_hint*/ None, Some(false))),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_defaults_missing_open_world_hint_to_true() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: true,
            destructive_enabled: true,
            open_world_enabled: false,
        }),
        apps: HashMap::new(),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/create",
        /*tool_title*/ None,
        Some(&annotations(Some(false), /*open_world_hint*/ None)),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_is_enabled_uses_default_for_unconfigured_apps() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: false,
            destructive_enabled: true,
            open_world_enabled: true,
        }),
        apps: HashMap::new(),
    };

    assert!(!app_is_enabled(&apps_config, Some("calendar")));
    assert!(!app_is_enabled(&apps_config, /*connector_id*/ None));
}

#[test]
fn app_is_enabled_prefers_per_app_override_over_default() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: false,
            destructive_enabled: true,
            open_world_enabled: true,
        }),
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: None,
                open_world_enabled: None,
                default_tools_approval_mode: None,
                default_tools_enabled: None,
                tools: None,
            },
        )]),
    };

    assert!(app_is_enabled(&apps_config, Some("calendar")));
    assert!(!app_is_enabled(&apps_config, Some("drive")));
}

#[test]
fn requirements_disabled_connector_overrides_enabled_connector() {
    let mut effective_apps = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "connector_123123".to_string(),
            AppConfig {
                enabled: true,
                ..Default::default()
            },
        )]),
    };
    let requirements_apps = AppsRequirementsToml {
        apps: BTreeMap::from([(
            "connector_123123".to_string(),
            AppRequirementToml {
                enabled: Some(false),
                tools: None,
            },
        )]),
    };

    apply_requirements_apps_constraints(&mut effective_apps, Some(&requirements_apps));

    assert_eq!(
        effective_apps
            .apps
            .get("connector_123123")
            .map(|app| app.enabled),
        Some(false)
    );
}

#[test]
fn requirements_enabled_does_not_override_disabled_connector() {
    let mut effective_apps = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "connector_123123".to_string(),
            AppConfig {
                enabled: false,
                ..Default::default()
            },
        )]),
    };
    let requirements_apps = AppsRequirementsToml {
        apps: BTreeMap::from([(
            "connector_123123".to_string(),
            AppRequirementToml {
                enabled: Some(true),
                tools: None,
            },
        )]),
    };

    apply_requirements_apps_constraints(&mut effective_apps, Some(&requirements_apps));

    assert_eq!(
        effective_apps
            .apps
            .get("connector_123123")
            .map(|app| app.enabled),
        Some(false)
    );
}

#[tokio::test]
async fn cloud_requirements_disable_connector_overrides_user_apps_config() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(
        codex_home.path().join(CONFIG_TOML_FILE),
        r#"
[apps.connector_123123]
enabled = true
"#,
    )
    .expect("write config");

    let requirements = ConfigRequirementsToml {
        apps: Some(AppsRequirementsToml {
            apps: BTreeMap::from([(
                "connector_123123".to_string(),
                AppRequirementToml {
                    enabled: Some(false),
                    tools: None,
                },
            )]),
        }),
        ..Default::default()
    };

    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .cloud_requirements(CloudRequirementsLoader::new(async move {
            Ok(Some(requirements))
        }))
        .build()
        .await
        .expect("config should build");

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "events.list",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[tokio::test]
async fn cloud_requirements_disable_connector_applies_without_user_apps_table() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(codex_home.path().join(CONFIG_TOML_FILE), "").expect("write config");

    let requirements = ConfigRequirementsToml {
        apps: Some(AppsRequirementsToml {
            apps: BTreeMap::from([(
                "connector_123123".to_string(),
                AppRequirementToml {
                    enabled: Some(false),
                    tools: None,
                },
            )]),
        }),
        ..Default::default()
    };

    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .cloud_requirements(CloudRequirementsLoader::new(async move {
            Ok(Some(requirements))
        }))
        .build()
        .await
        .expect("config should build");

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "events.list",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[tokio::test]
async fn local_requirements_disable_connector_overrides_user_apps_config() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let config_toml_path =
        AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)).expect("abs path");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .build()
        .await
        .expect("config should build");

    let requirements = ConfigRequirementsToml {
        apps: Some(AppsRequirementsToml {
            apps: BTreeMap::from([(
                "connector_123123".to_string(),
                AppRequirementToml {
                    enabled: Some(false),
                    tools: None,
                },
            )]),
        }),
        ..Default::default()
    };
    config.config_layer_stack =
        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
            .expect("requirements stack")
            .with_user_config(
                &config_toml_path,
                toml::from_str::<toml::Value>(
                    r#"
[apps.connector_123123]
enabled = true
"#,
                )
                .expect("apps config"),
            );

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "events.list",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[tokio::test]
async fn local_requirements_disable_connector_applies_without_user_apps_table() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .build()
        .await
        .expect("config should build");

    let requirements = ConfigRequirementsToml {
        apps: Some(AppsRequirementsToml {
            apps: BTreeMap::from([(
                "connector_123123".to_string(),
                AppRequirementToml {
                    enabled: Some(false),
                    tools: None,
                },
            )]),
        }),
        ..Default::default()
    };
    config.config_layer_stack =
        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
            .expect("requirements stack");

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "events.list",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[tokio::test]
async fn with_app_enabled_state_preserves_unrelated_disabled_connector() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .build()
        .await
        .expect("config should build");

    let requirements = ConfigRequirementsToml {
        apps: Some(AppsRequirementsToml {
            apps: BTreeMap::from([(
                "connector_drive".to_string(),
                AppRequirementToml {
                    enabled: Some(false),
                    tools: None,
                },
            )]),
        }),
        ..Default::default()
    };
    config.config_layer_stack =
        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
            .expect("requirements stack");

    let mut slack = app("connector_slack");
    slack.is_enabled = false;

    let mut drive = app("connector_drive");
    drive.is_enabled = false;

    assert_eq!(
        with_app_enabled_state(vec![slack.clone(), app("connector_drive")], &config),
        vec![slack, drive]
    );
}

#[test]
fn app_tool_policy_honors_default_app_enabled_false() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: false,
            destructive_enabled: true,
            open_world_enabled: true,
        }),
        apps: HashMap::new(),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/list",
        /*tool_title*/ None,
        Some(&annotations(
            /*destructive_hint*/ None, /*open_world_hint*/ None,
        )),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_uses_managed_approval_without_apps_config() {
    let policy = app_tool_policy_from_apps_config(
        /*apps_config*/ None,
        Some("calendar"),
        "events/list",
        /*tool_title*/ None,
        /*annotations*/ None,
        Some(AppToolApproval::Approve),
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Approve,
        }
    );
}

fn app_tool_requirements(
    app_id: &str,
    tool_name: &str,
    approval_mode: AppToolApproval,
) -> AppsRequirementsToml {
    AppsRequirementsToml {
        apps: BTreeMap::from([(
            app_id.to_string(),
            AppRequirementToml {
                enabled: None,
                tools: Some(AppToolsRequirementsToml {
                    tools: BTreeMap::from([(
                        tool_name.to_string(),
                        AppToolRequirementToml {
                            approval_mode: Some(approval_mode),
                        },
                    )]),
                }),
            },
        )]),
    }
}

#[test]
fn managed_app_tool_approval_uses_raw_tool_name() {
    let requirements_apps = app_tool_requirements(
        "connector_123123",
        "calendar/list_events",
        AppToolApproval::Approve,
    );

    assert_eq!(
        managed_app_tool_approval(
            Some(&requirements_apps),
            Some("connector_123123"),
            "calendar/list_events",
        ),
        Some(AppToolApproval::Approve)
    );
    assert_eq!(
        managed_app_tool_approval(
            Some(&requirements_apps),
            Some("connector_123123"),
            "calendar/create_event",
        ),
        None
    );
}

#[tokio::test]
async fn cloud_requirements_tool_approval_overrides_user_apps_config() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(
        codex_home.path().join(CONFIG_TOML_FILE),
        r#"
[apps.connector_123123.tools."calendar/list_events"]
approval_mode = "prompt"
"#,
    )
    .expect("write config");

    let requirements = ConfigRequirementsToml {
        apps: Some(app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Approve,
        )),
        ..Default::default()
    };

    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .cloud_requirements(CloudRequirementsLoader::new(async move {
            Ok(Some(requirements))
        }))
        .build()
        .await
        .expect("config should build");

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "calendar/list_events",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Approve,
        }
    );
}

#[tokio::test]
async fn local_requirements_tool_approval_overrides_user_apps_config() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let config_toml_path =
        AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)).expect("abs path");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .build()
        .await
        .expect("config should build");

    let requirements = ConfigRequirementsToml {
        apps: Some(app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Approve,
        )),
        ..Default::default()
    };
    config.config_layer_stack =
        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
            .expect("requirements stack")
            .with_user_config(
                &config_toml_path,
                toml::from_str::<toml::Value>(
                    r#"
[apps.connector_123123.tools."calendar/list_events"]
approval_mode = "prompt"
"#,
                )
                .expect("apps config"),
            );

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "calendar/list_events",
        /*tool_title*/ None,
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Approve,
        }
    );
}

#[tokio::test]
async fn local_requirements_tool_approval_does_not_match_tool_title() {
    let codex_home = tempdir().expect("tempdir should succeed");
    let mut config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .fallback_cwd(Some(codex_home.path().to_path_buf()))
        .build()
        .await
        .expect("config should build");

    let requirements = ConfigRequirementsToml {
        apps: Some(app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Approve,
        )),
        ..Default::default()
    };
    config.config_layer_stack =
        ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
            .expect("requirements stack");

    let policy = app_tool_policy(
        &config,
        Some("connector_123123"),
        "calendar/create_event",
        Some("calendar/list_events"),
        /*annotations*/ None,
    );
    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() {
    let apps_config = AppsConfigToml {
        default: Some(AppsDefaultConfig {
            enabled: false,
            destructive_enabled: true,
            open_world_enabled: true,
        }),
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: None,
                open_world_enabled: None,
                default_tools_approval_mode: None,
                default_tools_enabled: None,
                tools: None,
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/list",
        /*tool_title*/ None,
        Some(&annotations(
            /*destructive_hint*/ None, /*open_world_hint*/ None,
        )),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() {
    let apps_config = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: Some(false),
                open_world_enabled: Some(false),
                default_tools_approval_mode: None,
                default_tools_enabled: None,
                tools: Some(AppToolsConfig {
                    tools: HashMap::from([(
                        "events/create".to_string(),
                        AppToolConfig {
                            enabled: Some(true),
                            approval_mode: None,
                        },
                    )]),
                }),
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/create",
        /*tool_title*/ None,
        Some(&annotations(Some(true), Some(true))),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() {
    let apps_config = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: Some(false),
                open_world_enabled: Some(false),
                default_tools_approval_mode: None,
                default_tools_enabled: Some(true),
                tools: None,
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/create",
        /*tool_title*/ None,
        Some(&annotations(Some(true), Some(true))),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Auto,
        }
    );
}

#[test]
fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() {
    let apps_config = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: Some(true),
                open_world_enabled: Some(true),
                default_tools_approval_mode: Some(AppToolApproval::Approve),
                default_tools_enabled: Some(false),
                tools: None,
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/list",
        /*tool_title*/ None,
        Some(&annotations(
            /*destructive_hint*/ None, /*open_world_hint*/ None,
        )),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: false,
            approval: AppToolApproval::Approve,
        }
    );
}

#[test]
fn app_tool_policy_uses_default_tools_approval_mode() {
    let apps_config = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: None,
                open_world_enabled: None,
                default_tools_approval_mode: Some(AppToolApproval::Prompt),
                default_tools_enabled: None,
                tools: Some(AppToolsConfig {
                    tools: HashMap::new(),
                }),
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "events/list",
        /*tool_title*/ None,
        Some(&annotations(
            /*destructive_hint*/ None, /*open_world_hint*/ None,
        )),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Prompt,
        }
    );
}

#[test]
fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() {
    let apps_config = AppsConfigToml {
        default: None,
        apps: HashMap::from([(
            "calendar".to_string(),
            AppConfig {
                enabled: true,
                destructive_enabled: Some(false),
                open_world_enabled: Some(false),
                default_tools_approval_mode: Some(AppToolApproval::Auto),
                default_tools_enabled: Some(false),
                tools: Some(AppToolsConfig {
                    tools: HashMap::from([(
                        "events/create".to_string(),
                        AppToolConfig {
                            enabled: Some(true),
                            approval_mode: Some(AppToolApproval::Approve),
                        },
                    )]),
                }),
            },
        )]),
    };

    let policy = app_tool_policy_from_apps_config(
        Some(&apps_config),
        Some("calendar"),
        "calendar_events/create",
        Some("events/create"),
        Some(&annotations(Some(true), Some(true))),
        /*managed_approval*/ None,
    );

    assert_eq!(
        policy,
        AppToolPolicy {
            enabled: true,
            approval: AppToolApproval::Approve,
        }
    );
}

#[tokio::test]
async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(
        codex_home.path().join(CONFIG_TOML_FILE),
        r#"
[tool_suggest]
discoverables = [
  { type = "connector", id = "connector_2128aebfecb84f64a069897515042a44" },
  { type = "plugin", id = "slack@openai-curated" },
  { type = "connector", id = "   " }
]
"#,
    )
    .expect("write config");
    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .build()
        .await
        .expect("config should load");

    assert_eq!(
        tool_suggest_connector_ids(&config).await,
        HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()])
    );
}

#[tokio::test]
async fn tool_suggest_connector_ids_exclude_disabled_tool_suggestions() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(
        codex_home.path().join(CONFIG_TOML_FILE),
        r#"
[tool_suggest]
discoverables = [
  { type = "connector", id = "connector_calendar" },
  { type = "connector", id = "connector_gmail" }
]
disabled_tools = [
  { type = "connector", id = "connector_calendar" }
]
"#,
    )
    .expect("write config");
    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .build()
        .await
        .expect("config should load");

    assert_eq!(
        tool_suggest_connector_ids(&config).await,
        HashSet::from(["connector_gmail".to_string()])
    );
}

#[tokio::test]
async fn tool_suggest_uses_connector_id_fallback_when_directory_cache_is_empty() {
    let codex_home = tempdir().expect("tempdir should succeed");
    std::fs::write(
        codex_home.path().join(CONFIG_TOML_FILE),
        r#"
[features]
apps = true

[tool_suggest]
discoverables = [
  { type = "connector", id = "connector_gmail" }
]
"#,
    )
    .expect("write config");
    let config = ConfigBuilder::default()
        .codex_home(codex_home.path().to_path_buf())
        .build()
        .await
        .expect("config should load");
    let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();

    let discoverable_tools =
        list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[])
            .await
            .expect("discoverable tools should load");

    assert_eq!(
        discoverable_tools,
        vec![DiscoverableTool::from(plugin_connector_to_app_info(
            "connector_gmail".to_string(),
        ))]
    );
}
