use crate::history_cell::PlainHistoryCell;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::Permissions;
use crate::session_state::SessionNetworkProxyRuntime;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use codex_config::ManagedHooksRequirementsToml;
use codex_config::NetworkConstraints;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkUnixSocketPermissionToml;
use codex_config::RequirementSource;
use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
use codex_config::WebSearchModeRequirement;
use codex_config::format_config_layer_source;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use ratatui::style::Stylize;
use ratatui::text::Line;
use toml::Value as TomlValue;

pub(crate) fn new_debug_config_output(
    config: &Config,
    session_network_proxy: Option<&SessionNetworkProxyRuntime>,
) -> PlainHistoryCell {
    let mut lines = render_debug_config_lines(&config.config_layer_stack, |mode| {
        sandbox_mode_is_allowed_by_permissions(&config.permissions, mode)
    });

    if let Some(proxy) = session_network_proxy {
        lines.push("".into());
        lines.push("Session runtime:".bold().into());
        lines.push("  - network_proxy".into());
        let SessionNetworkProxyRuntime {
            http_addr,
            socks_addr,
        } = proxy;
        let all_proxy = session_all_proxy_url(
            http_addr,
            socks_addr,
            config
                .permissions
                .network
                .as_ref()
                .is_some_and(crate::legacy_core::config::NetworkProxySpec::socks_enabled),
        );
        lines.push(format!("    - HTTP_PROXY  = http://{http_addr}").into());
        lines.push(format!("    - ALL_PROXY   = {all_proxy}").into());
    }

    PlainHistoryCell::new(lines)
}

fn sandbox_mode_is_allowed_by_permissions(
    permissions: &Permissions,
    mode: SandboxModeRequirement,
) -> bool {
    let permission_profile = match mode {
        SandboxModeRequirement::ReadOnly => PermissionProfile::read_only(),
        SandboxModeRequirement::WorkspaceWrite => PermissionProfile::workspace_write(),
        SandboxModeRequirement::DangerFullAccess => PermissionProfile::Disabled,
        SandboxModeRequirement::ExternalSandbox => PermissionProfile::External {
            network: NetworkSandboxPolicy::Restricted,
        },
    };

    permissions
        .can_set_permission_profile(&permission_profile)
        .is_ok()
}

fn session_all_proxy_url(http_addr: &str, socks_addr: &str, socks_enabled: bool) -> String {
    if socks_enabled {
        format!("socks5h://{socks_addr}")
    } else {
        format!("http://{http_addr}")
    }
}

fn render_debug_config_lines(
    stack: &ConfigLayerStack,
    sandbox_mode_is_effectively_allowed: impl Fn(SandboxModeRequirement) -> bool,
) -> Vec<Line<'static>> {
    let mut lines = vec!["/debug-config".magenta().into(), "".into()];

    lines.push(
        "Config layer stack (lowest precedence first):"
            .bold()
            .into(),
    );
    let layers = stack.get_layers(
        ConfigLayerStackOrdering::LowestPrecedenceFirst,
        /*include_disabled*/ true,
    );
    if layers.is_empty() {
        lines.push("  <none>".dim().into());
    } else {
        for (index, layer) in layers.iter().enumerate() {
            let source = format_config_layer_source(&layer.name, CONFIG_TOML_FILE);
            let status = if layer.is_disabled() {
                "disabled"
            } else {
                "enabled"
            };
            lines.push(format!("  {}. {source} ({status})", index + 1).into());
            lines.extend(render_non_file_layer_details(layer));
            if let Some(reason) = &layer.disabled_reason {
                lines.push(format!("     reason: {reason}").dim().into());
            }
        }
    }

    let requirements = stack.requirements();
    let requirements_toml = stack.requirements_toml();

    lines.push("".into());
    lines.push("Requirements:".bold().into());
    let mut requirement_lines = Vec::new();

    if let Some(policies) = requirements_toml.allowed_approval_policies.as_ref() {
        let value = join_or_empty(policies.iter().map(ToString::to_string).collect::<Vec<_>>());
        requirement_lines.push(requirement_line(
            "allowed_approval_policies",
            value,
            requirements.approval_policy.source.as_ref(),
        ));
    }

    if let Some(reviewers) = requirements_toml.allowed_approvals_reviewers.as_ref() {
        let value = join_or_empty(
            reviewers
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>(),
        );
        requirement_lines.push(requirement_line(
            "allowed_approvals_reviewers",
            value,
            requirements.approvals_reviewer.source.as_ref(),
        ));
    }

    if let Some(modes) = requirements_toml.allowed_sandbox_modes.as_ref() {
        let value = join_or_empty(
            modes
                .iter()
                .copied()
                .filter(|mode| sandbox_mode_is_effectively_allowed(*mode))
                .map(format_sandbox_mode_requirement)
                .collect::<Vec<_>>(),
        );
        requirement_lines.push(requirement_line(
            "allowed_sandbox_modes",
            value,
            requirements.permission_profile.source.as_ref(),
        ));
    }

    if let Some(modes) = requirements_toml.allowed_web_search_modes.as_ref() {
        let normalized = normalize_allowed_web_search_modes(modes);
        let value = join_or_empty(
            normalized
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>(),
        );
        requirement_lines.push(requirement_line(
            "allowed_web_search_modes",
            value,
            requirements.web_search_mode.source.as_ref(),
        ));
    }

    if let Some(allow_managed_hooks_only) = requirements_toml.allow_managed_hooks_only {
        requirement_lines.push(requirement_line(
            "allow_managed_hooks_only",
            allow_managed_hooks_only.to_string(),
            requirements
                .allow_managed_hooks_only
                .as_ref()
                .map(|sourced| &sourced.source),
        ));
    }

    if let Some(allow_appshots) = requirements_toml.allow_appshots {
        requirement_lines.push(requirement_line(
            "allow_appshots",
            allow_appshots.to_string(),
            requirements
                .allow_appshots
                .as_ref()
                .map(|sourced| &sourced.source),
        ));
    }

    if let Some(allow_remote_control) = requirements_toml.allow_remote_control {
        requirement_lines.push(requirement_line(
            "allow_remote_control",
            allow_remote_control.to_string(),
            requirements
                .allow_remote_control
                .as_ref()
                .map(|sourced| &sourced.source),
        ));
    }

    if requirements_toml.guardian_policy_config.is_some() {
        requirement_lines.push(requirement_line(
            "guardian_policy_config",
            "configured".to_string(),
            requirements.guardian_policy_config_source.as_ref(),
        ));
    }

    if let Some(feature_requirements) = requirements.feature_requirements.as_ref() {
        let value = join_or_empty(
            feature_requirements
                .value
                .entries
                .iter()
                .map(|(feature, enabled)| format!("{feature}={enabled}"))
                .collect::<Vec<_>>(),
        );
        requirement_lines.push(requirement_line(
            "features",
            value,
            Some(&feature_requirements.source),
        ));
    }

    if let Some(hooks) = requirements_toml.hooks.as_ref() {
        requirement_lines.push(requirement_line(
            "hooks",
            format_managed_hooks_requirements(hooks),
            requirements
                .managed_hooks
                .as_ref()
                .and_then(|managed_hooks| managed_hooks.source.as_ref()),
        ));
    }

    if let Some(servers) = requirements_toml.mcp_servers.as_ref() {
        let value = join_or_empty(servers.keys().cloned().collect::<Vec<_>>());
        requirement_lines.push(requirement_line(
            "mcp_servers",
            value,
            requirements
                .mcp_servers
                .as_ref()
                .map(|sourced| &sourced.source),
        ));
    }

    // TODO(gt): Expand this debug output with detailed skills and rules display.
    if requirements_toml.rules.is_some() {
        requirement_lines.push(requirement_line(
            "rules",
            "configured".to_string(),
            requirements.exec_policy_source(),
        ));
    }

    if let Some(residency) = requirements_toml.enforce_residency {
        requirement_lines.push(requirement_line(
            "enforce_residency",
            format_residency_requirement(residency),
            requirements.enforce_residency.source.as_ref(),
        ));
    }

    if let Some(network) = requirements.network.as_ref() {
        requirement_lines.push(requirement_line(
            "experimental_network",
            format_network_constraints(&network.value),
            Some(&network.source),
        ));
    }

    if let Some(filesystem) = requirements.filesystem.as_ref() {
        let deny_read = join_or_empty(
            filesystem
                .value
                .deny_read
                .iter()
                .map(|pattern| pattern.as_str().to_string())
                .collect::<Vec<_>>(),
        );
        requirement_lines.push(requirement_line(
            "permissions.filesystem.deny_read",
            deny_read,
            Some(&filesystem.source),
        ));
    }

    if requirement_lines.is_empty() {
        lines.push("  <none>".dim().into());
    } else {
        lines.extend(requirement_lines);
    }

    lines
}

fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
    match &layer.name {
        ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config),
        ConfigLayerSource::Mdm { .. }
        | ConfigLayerSource::EnterpriseManaged { .. }
        | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
        ConfigLayerSource::System { .. }
        | ConfigLayerSource::User { .. }
        | ConfigLayerSource::Project { .. }
        | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(),
    }
}

fn render_session_flag_details(config: &TomlValue) -> Vec<Line<'static>> {
    let mut pairs = Vec::new();
    flatten_toml_key_values(config, /*prefix*/ None, &mut pairs);

    if pairs.is_empty() {
        return vec!["     - <none>".dim().into()];
    }

    pairs
        .into_iter()
        .map(|(key, value)| format!("     - {key} = {value}").into())
        .collect()
}

fn format_managed_hooks_requirements(hooks: &ManagedHooksRequirementsToml) -> String {
    let mut parts = Vec::new();

    if let Some(managed_dir) = hooks.managed_dir.as_ref() {
        parts.push(format!("managed_dir={}", managed_dir.display()));
    }
    if let Some(windows_managed_dir) = hooks.windows_managed_dir.as_ref() {
        parts.push(format!(
            "windows_managed_dir={}",
            windows_managed_dir.display()
        ));
    }
    parts.push(format!("handlers={}", hooks.handler_count()));

    join_or_empty(parts)
}

fn render_non_file_layer_value(layer: &ConfigLayerEntry) -> Vec<Line<'static>> {
    let label = non_file_layer_value_label(&layer.name);
    let value = layer
        .raw_toml()
        .map(ToString::to_string)
        .unwrap_or_else(|| format_toml_value(&layer.config));
    if value.is_empty() {
        return vec![format!("     {label}: <empty>").dim().into()];
    }

    if value.contains('\n') {
        let mut lines = vec![format!("     {label}:").into()];
        lines.extend(value.lines().map(|line| format!("       {line}").into()));
        lines
    } else {
        vec![format!("     {label}: {value}").into()]
    }
}

fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
    match source {
        ConfigLayerSource::Mdm { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
            "MDM value"
        }
        ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
        ConfigLayerSource::SessionFlags
        | ConfigLayerSource::System { .. }
        | ConfigLayerSource::User { .. }
        | ConfigLayerSource::Project { .. }
        | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => "Layer value",
    }
}

fn flatten_toml_key_values(
    value: &TomlValue,
    prefix: Option<&str>,
    out: &mut Vec<(String, String)>,
) {
    match value {
        TomlValue::Table(table) => {
            let mut entries = table.iter().collect::<Vec<_>>();
            entries.sort_by_key(|(key, _)| key.as_str());
            for (key, child) in entries {
                let next_prefix = if let Some(prefix) = prefix {
                    format!("{prefix}.{key}")
                } else {
                    key.to_string()
                };
                flatten_toml_key_values(child, Some(&next_prefix), out);
            }
        }
        _ => {
            let key = prefix.unwrap_or("<value>").to_string();
            out.push((key, format_toml_value(value)));
        }
    }
}

fn format_toml_value(value: &TomlValue) -> String {
    value.to_string()
}

fn requirement_line(
    name: &str,
    value: String,
    source: Option<&RequirementSource>,
) -> Line<'static> {
    let source = source
        .map(ToString::to_string)
        .unwrap_or_else(|| "<unspecified>".to_string());
    format!("  - {name}: {value} (source: {source})").into()
}

fn join_or_empty(values: Vec<String>) -> String {
    if values.is_empty() {
        "<empty>".to_string()
    } else {
        values.join(", ")
    }
}

fn normalize_allowed_web_search_modes(
    modes: &[WebSearchModeRequirement],
) -> Vec<WebSearchModeRequirement> {
    if modes.is_empty() {
        return vec![WebSearchModeRequirement::Disabled];
    }

    let mut normalized = modes.to_vec();
    if !normalized.contains(&WebSearchModeRequirement::Disabled) {
        normalized.push(WebSearchModeRequirement::Disabled);
    }
    normalized
}

fn format_sandbox_mode_requirement(mode: SandboxModeRequirement) -> String {
    match mode {
        SandboxModeRequirement::ReadOnly => "read-only".to_string(),
        SandboxModeRequirement::WorkspaceWrite => "workspace-write".to_string(),
        SandboxModeRequirement::DangerFullAccess => "danger-full-access".to_string(),
        SandboxModeRequirement::ExternalSandbox => "external-sandbox".to_string(),
    }
}

fn format_residency_requirement(requirement: ResidencyRequirement) -> String {
    match requirement {
        ResidencyRequirement::Us => "us".to_string(),
    }
}

fn format_network_constraints(network: &NetworkConstraints) -> String {
    let mut parts = Vec::new();

    let NetworkConstraints {
        enabled,
        http_port,
        socks_port,
        allow_upstream_proxy,
        dangerously_allow_non_loopback_proxy,
        dangerously_allow_all_unix_sockets,
        domains,
        managed_allowed_domains_only,
        unix_sockets,
        allow_local_binding,
    } = network;

    if let Some(enabled) = enabled {
        parts.push(format!("enabled={enabled}"));
    }
    if let Some(http_port) = http_port {
        parts.push(format!("http_port={http_port}"));
    }
    if let Some(socks_port) = socks_port {
        parts.push(format!("socks_port={socks_port}"));
    }
    if let Some(allow_upstream_proxy) = allow_upstream_proxy {
        parts.push(format!("allow_upstream_proxy={allow_upstream_proxy}"));
    }
    if let Some(dangerously_allow_non_loopback_proxy) = dangerously_allow_non_loopback_proxy {
        parts.push(format!(
            "dangerously_allow_non_loopback_proxy={dangerously_allow_non_loopback_proxy}"
        ));
    }
    if let Some(dangerously_allow_all_unix_sockets) = dangerously_allow_all_unix_sockets {
        parts.push(format!(
            "dangerously_allow_all_unix_sockets={dangerously_allow_all_unix_sockets}"
        ));
    }
    if let Some(domains) = domains {
        parts.push(format!(
            "domains={}",
            format_network_permission_entries(&domains.entries, format_network_domain_permission)
        ));
    }
    if let Some(managed_allowed_domains_only) = managed_allowed_domains_only {
        parts.push(format!(
            "managed_allowed_domains_only={managed_allowed_domains_only}"
        ));
    }
    if let Some(unix_sockets) = unix_sockets {
        parts.push(format!(
            "unix_sockets={}",
            format_network_permission_entries(
                &unix_sockets.entries,
                format_network_unix_socket_permission,
            )
        ));
    }
    if let Some(allow_local_binding) = allow_local_binding {
        parts.push(format!("allow_local_binding={allow_local_binding}"));
    }

    join_or_empty(parts)
}

fn format_network_permission_entries<T: Copy>(
    entries: &std::collections::BTreeMap<String, T>,
    format_value: impl Fn(T) -> &'static str,
) -> String {
    let parts = entries
        .iter()
        .map(|(key, value)| format!("{key}={}", format_value(*value)))
        .collect::<Vec<_>>();
    format!("{{{}}}", parts.join(", "))
}

fn format_network_domain_permission(permission: NetworkDomainPermissionToml) -> &'static str {
    match permission {
        NetworkDomainPermissionToml::Allow => "allow",
        NetworkDomainPermissionToml::Deny => "deny",
    }
}

fn format_network_unix_socket_permission(
    permission: NetworkUnixSocketPermissionToml,
) -> &'static str {
    match permission {
        NetworkUnixSocketPermissionToml::Allow => "allow",
        NetworkUnixSocketPermissionToml::Deny => "deny",
    }
}

#[cfg(test)]
mod tests {
    use super::render_debug_config_lines;
    use super::sandbox_mode_is_allowed_by_permissions;
    use super::session_all_proxy_url;
    use crate::legacy_core::config::Permissions;
    use codex_app_server_protocol::AskForApproval;
    use codex_app_server_protocol::ConfigLayerSource;
    use codex_config::ConfigLayerEntry;
    use codex_config::ConfigLayerStack;
    use codex_config::ConfigRequirements;
    use codex_config::ConfigRequirementsToml;
    use codex_config::Constrained;
    use codex_config::ConstrainedWithSource;
    use codex_config::ConstraintError;
    use codex_config::FeatureRequirementsToml;
    use codex_config::FilesystemConstraints;
    use codex_config::HookEventsToml;
    use codex_config::HookHandlerConfig;
    use codex_config::ManagedHooksRequirementsToml;
    use codex_config::MatcherGroup;
    use codex_config::McpServerIdentity;
    use codex_config::McpServerRequirement;
    use codex_config::NetworkConstraints;
    use codex_config::NetworkDomainPermissionToml;
    use codex_config::NetworkDomainPermissionsToml;
    use codex_config::NetworkUnixSocketPermissionToml;
    use codex_config::NetworkUnixSocketPermissionsToml;
    use codex_config::RequirementSource;
    use codex_config::ResidencyRequirement;
    use codex_config::SandboxModeRequirement;
    use codex_config::Sourced;
    use codex_config::WebSearchModeRequirement;
    use codex_config::sandbox_mode_requirement_for_permission_profile;
    use codex_protocol::config_types::ApprovalsReviewer;
    use codex_protocol::config_types::WebSearchMode;
    use codex_protocol::models::PermissionProfile;
    use codex_utils_absolute_path::AbsolutePathBuf;
    use ratatui::text::Line;
    use std::collections::BTreeMap;
    use toml::Value as TomlValue;

    fn empty_toml_table() -> TomlValue {
        TomlValue::Table(toml::map::Map::new())
    }

    fn absolute_path(path: &str) -> AbsolutePathBuf {
        AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
    }

    fn render_to_text(lines: &[Line<'static>]) -> String {
        lines
            .iter()
            .map(|line| {
                line.spans
                    .iter()
                    .map(|span| span.content.as_ref())
                    .collect::<String>()
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    fn render_stack_to_text(stack: &ConfigLayerStack) -> String {
        render_stack_to_text_with_sandbox_mode_filter(stack, |_| true)
    }

    fn render_stack_to_text_with_sandbox_mode_filter(
        stack: &ConfigLayerStack,
        sandbox_mode_is_effectively_allowed: impl Fn(SandboxModeRequirement) -> bool,
    ) -> String {
        render_to_text(&render_debug_config_lines(
            stack,
            sandbox_mode_is_effectively_allowed,
        ))
    }

    #[test]
    fn debug_config_output_lists_all_layers_including_disabled() {
        let system_file = if cfg!(windows) {
            absolute_path("C:\\etc\\codex\\config.toml")
        } else {
            absolute_path("/etc/codex/config.toml")
        };
        let project_folder = if cfg!(windows) {
            absolute_path("C:\\repo\\.codex")
        } else {
            absolute_path("/repo/.codex")
        };

        let layers = vec![
            ConfigLayerEntry::new(
                ConfigLayerSource::System { file: system_file },
                empty_toml_table(),
            ),
            ConfigLayerEntry::new_disabled(
                ConfigLayerSource::Project {
                    dot_codex_folder: project_folder,
                },
                empty_toml_table(),
                "project is untrusted",
            ),
        ];
        let stack = ConfigLayerStack::new(
            layers,
            ConfigRequirements::default(),
            ConfigRequirementsToml::default(),
        )
        .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        assert!(rendered.contains("(enabled)"));
        assert!(rendered.contains("(disabled)"));
        assert!(rendered.contains("reason: project is untrusted"));
        assert!(rendered.contains("Requirements:"));
        assert!(rendered.contains("  <none>"));
    }

    #[test]
    fn debug_config_output_lists_requirement_sources() {
        let requirements_file = if cfg!(windows) {
            absolute_path("C:\\ProgramData\\OpenAI\\Codex\\requirements.toml")
        } else {
            absolute_path("/etc/codex/requirements.toml")
        };
        let denied_path = if cfg!(windows) {
            absolute_path("C:\\Users\\alice\\.gitconfig")
        } else {
            absolute_path("/home/alice/.gitconfig")
        };

        let requirements = ConfigRequirements {
            approval_policy: ConstrainedWithSource::new(
                Constrained::allow_any(AskForApproval::OnRequest.to_core()),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            approvals_reviewer: ConstrainedWithSource::new(
                Constrained::allow_any(ApprovalsReviewer::AutoReview),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            permission_profile: ConstrainedWithSource::new(
                Constrained::allow_any(PermissionProfile::read_only()),
                Some(RequirementSource::SystemRequirementsToml {
                    file: requirements_file.clone(),
                }),
            ),
            mcp_servers: Some(Sourced::new(
                BTreeMap::from([(
                    "docs".to_string(),
                    McpServerRequirement {
                        identity: McpServerIdentity::Command {
                            command: "codex-mcp".to_string(),
                        },
                    },
                )]),
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            enforce_residency: ConstrainedWithSource::new(
                Constrained::allow_any(Some(ResidencyRequirement::Us)),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            web_search_mode: ConstrainedWithSource::new(
                Constrained::allow_any(WebSearchMode::Cached),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            allow_managed_hooks_only: Some(Sourced::new(
                /*value*/ true,
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            allow_appshots: Some(Sourced::new(
                /*value*/ false,
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            allow_remote_control: Some(Sourced::new(
                /*value*/ false,
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            feature_requirements: Some(Sourced::new(
                FeatureRequirementsToml {
                    entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
                },
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            network: Some(Sourced::new(
                NetworkConstraints {
                    enabled: Some(true),
                    domains: Some(NetworkDomainPermissionsToml {
                        entries: BTreeMap::from([(
                            "example.com".to_string(),
                            NetworkDomainPermissionToml::Allow,
                        )]),
                    }),
                    ..Default::default()
                },
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            filesystem: Some(Sourced::new(
                FilesystemConstraints {
                    deny_read: vec![denied_path.clone().into()],
                },
                RequirementSource::SystemRequirementsToml {
                    file: requirements_file.clone(),
                },
            )),
            guardian_policy_config_source: Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ..ConfigRequirements::default()
        };

        let requirements_toml = ConfigRequirementsToml {
            allowed_approval_policies: Some(vec![AskForApproval::OnRequest.to_core()]),
            allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
            allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
            allowed_permission_profiles: None,
            default_permissions: None,
            remote_sandbox_config: None,
            allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
            allow_managed_hooks_only: Some(true),
            allow_appshots: Some(false),
            allow_remote_control: Some(false),
            computer_use: None,
            windows: None,
            guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
            feature_requirements: Some(FeatureRequirementsToml {
                entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
            }),
            hooks: None,
            mcp_servers: Some(BTreeMap::from([(
                "docs".to_string(),
                McpServerRequirement {
                    identity: McpServerIdentity::Command {
                        command: "codex-mcp".to_string(),
                    },
                },
            )])),
            plugins: None,
            apps: None,
            rules: None,
            enforce_residency: Some(ResidencyRequirement::Us),
            network: None,
            permissions: None,
        };

        let user_file = if cfg!(windows) {
            absolute_path("C:\\users\\alice\\.codex\\config.toml")
        } else {
            absolute_path("/home/alice/.codex/config.toml")
        };
        let stack = ConfigLayerStack::new(
            vec![ConfigLayerEntry::new(
                ConfigLayerSource::User {
                    file: user_file,
                    profile: None,
                },
                empty_toml_table(),
            )],
            requirements,
            requirements_toml,
        )
        .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        #[cfg(not(windows))]
        insta::assert_snapshot!("debug_config_requirement_sources", rendered.as_str());

        let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
        assert!(rendered.contains(&format!(
            "allowed_approval_policies: on-request (source: {requirements_source})"
        )));
        assert!(rendered.contains(
            "allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))"
        ));
        assert!(
            rendered.contains(
                format!(
                    "allowed_sandbox_modes: read-only (source: {})",
                    requirements_file.as_path().display()
                )
                .as_str(),
            )
        );
        assert!(rendered.contains(&format!(
            "allowed_web_search_modes: cached, disabled (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "allow_managed_hooks_only: true (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "allow_appshots: false (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "allow_remote_control: false (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "guardian_policy_config: configured (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "features: guardian_approval=true (source: {requirements_source})"
        )));
        assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
        assert!(rendered.contains(&format!(
            "enforce_residency: us (source: {requirements_source})"
        )));
        assert!(rendered.contains(&format!(
            "experimental_network: enabled=true, domains={{example.com=allow}} (source: {requirements_source})"
        )));
        assert!(
            rendered.contains(
                format!(
                    "permissions.filesystem.deny_read: {}",
                    denied_path.as_path().display()
                )
                .as_str()
            )
        );
        assert!(!rendered.contains("  - rules:"));
    }

    #[test]
    fn debug_config_output_filters_sandbox_modes_blocked_by_deny_read_requirements() {
        let requirements_file = if cfg!(windows) {
            absolute_path("C:\\ProgramData\\OpenAI\\Codex\\requirements.toml")
        } else {
            absolute_path("/etc/codex/requirements.toml")
        };
        let denied_path = if cfg!(windows) {
            absolute_path("C:\\Users\\alice\\.gitconfig")
        } else {
            absolute_path("/home/alice/.gitconfig")
        };

        let requirements = ConfigRequirements {
            permission_profile: ConstrainedWithSource::new(
                Constrained::allow_any(PermissionProfile::read_only()),
                Some(RequirementSource::SystemRequirementsToml {
                    file: requirements_file.clone(),
                }),
            ),
            filesystem: Some(Sourced::new(
                FilesystemConstraints {
                    deny_read: vec![denied_path.into()],
                },
                RequirementSource::SystemRequirementsToml {
                    file: requirements_file.clone(),
                },
            )),
            ..ConfigRequirements::default()
        };
        let requirements_toml = ConfigRequirementsToml {
            allowed_sandbox_modes: Some(vec![
                SandboxModeRequirement::ReadOnly,
                SandboxModeRequirement::WorkspaceWrite,
                SandboxModeRequirement::DangerFullAccess,
                SandboxModeRequirement::ExternalSandbox,
            ]),
            ..ConfigRequirementsToml::default()
        };
        let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
            .expect("config layer stack");
        let constrained_permission_profile =
            Constrained::new(PermissionProfile::read_only(), |candidate| {
                let mode = sandbox_mode_requirement_for_permission_profile(candidate);
                match mode {
                    SandboxModeRequirement::ReadOnly | SandboxModeRequirement::WorkspaceWrite => {
                        Ok(())
                    }
                    SandboxModeRequirement::DangerFullAccess
                    | SandboxModeRequirement::ExternalSandbox => {
                        Err(ConstraintError::InvalidValue {
                            field_name: "sandbox_mode",
                            candidate: format!("{mode:?}"),
                            allowed: "[read-only, workspace-write]".to_string(),
                            requirement_source: RequirementSource::Unknown,
                        })
                    }
                }
            })
            .expect("constrained permission profile");
        let permissions = Permissions::from_approval_and_profile(
            Constrained::allow_any(AskForApproval::OnRequest.to_core()),
            constrained_permission_profile,
        )
        .expect("permissions");

        let rendered = render_stack_to_text_with_sandbox_mode_filter(&stack, |mode| {
            sandbox_mode_is_allowed_by_permissions(&permissions, mode)
        });
        #[cfg(not(windows))]
        insta::assert_snapshot!(
            "debug_config_effective_sandbox_modes_with_deny_read",
            rendered.as_str()
        );
        assert!(
            rendered.contains(
                format!(
                    "allowed_sandbox_modes: read-only, workspace-write (source: {})",
                    requirements_file.as_path().display()
                )
                .as_str()
            )
        );
        assert!(!rendered.contains("danger-full-access"));
        assert!(!rendered.contains("external-sandbox"));
    }

    #[test]
    fn debug_config_output_lists_approvals_reviewer_as_requirement() {
        let requirements = ConfigRequirements {
            approvals_reviewer: ConstrainedWithSource::new(
                Constrained::allow_any(ApprovalsReviewer::AutoReview),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            ..ConfigRequirements::default()
        };
        let requirements_toml = ConfigRequirementsToml {
            allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
            ..ConfigRequirementsToml::default()
        };
        let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
            .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        assert!(rendered.contains(
            "allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))"
        ));
        assert!(!rendered.contains("Requirements:\n  <none>"));
    }

    #[test]
    fn debug_config_output_formats_unix_socket_permissions() {
        let requirements = ConfigRequirements {
            network: Some(Sourced::new(
                NetworkConstraints {
                    unix_sockets: Some(NetworkUnixSocketPermissionsToml {
                        entries: BTreeMap::from([
                            (
                                "/tmp/codex.sock".to_string(),
                                NetworkUnixSocketPermissionToml::Allow,
                            ),
                            (
                                "/tmp/blocked.sock".to_string(),
                                NetworkUnixSocketPermissionToml::Deny,
                            ),
                        ]),
                    }),
                    ..Default::default()
                },
                RequirementSource::LegacyManagedConfigTomlFromMdm,
            )),
            ..ConfigRequirements::default()
        };

        let stack =
            ConfigLayerStack::new(Vec::new(), requirements, ConfigRequirementsToml::default())
                .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
        assert!(rendered.contains(&format!(
            "experimental_network: unix_sockets={{/tmp/blocked.sock=deny, /tmp/codex.sock=allow}} (source: {requirements_source})"
        )));
    }

    #[test]
    fn debug_config_output_lists_session_flag_key_value_pairs() {
        let session_flags = toml::from_str::<TomlValue>(
            r#"
model = "gpt-5"
[sandbox_workspace_write]
network_access = true
writable_roots = ["/tmp"]
"#,
        )
        .expect("session flags");

        let stack = ConfigLayerStack::new(
            vec![ConfigLayerEntry::new(
                ConfigLayerSource::SessionFlags,
                session_flags,
            )],
            ConfigRequirements::default(),
            ConfigRequirementsToml::default(),
        )
        .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        assert!(rendered.contains("session-flags (enabled)"));
        assert!(rendered.contains("     - model = \"gpt-5\""));
        assert!(rendered.contains("     - sandbox_workspace_write.network_access = true"));
        assert!(rendered.contains("sandbox_workspace_write.writable_roots"));
        assert!(rendered.contains("/tmp"));
    }

    #[test]
    fn debug_config_output_shows_legacy_mdm_layer_value() {
        let raw_mdm_toml = r#"
# managed by MDM
model = "managed_model"
approval_policy = "never"
"#;
        let mdm_value = toml::from_str::<TomlValue>(raw_mdm_toml).expect("MDM value");
        let mdm_base_dir = if cfg!(windows) {
            absolute_path("C:\\codex")
        } else {
            absolute_path("/var/lib/codex")
        };

        let stack = ConfigLayerStack::new(
            vec![ConfigLayerEntry::new_with_raw_toml(
                ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
                mdm_value,
                raw_mdm_toml.to_string(),
                mdm_base_dir,
            )],
            ConfigRequirements::default(),
            ConfigRequirementsToml::default(),
        )
        .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        assert!(rendered.contains("legacy managed_config.toml (MDM) (enabled)"));
        assert!(rendered.contains("MDM value:"));
        assert!(rendered.contains("# managed by MDM"));
        assert!(rendered.contains("model = \"managed_model\""));
        assert!(rendered.contains("approval_policy = \"never\""));
    }

    #[test]
    fn debug_config_output_shows_enterprise_managed_layer_value() {
        let raw_cloud_toml = r#"
# managed by cloud
model = "enterprise_model"
approval_policy = "never"
"#;
        let cloud_value = toml::from_str::<TomlValue>(raw_cloud_toml).expect("cloud value");
        let cloud_base_dir = if cfg!(windows) {
            absolute_path("C:\\codex")
        } else {
            absolute_path("/var/lib/codex")
        };

        let stack = ConfigLayerStack::new(
            vec![ConfigLayerEntry::new_with_raw_toml(
                ConfigLayerSource::EnterpriseManaged {
                    id: "cfg_123".to_string(),
                    name: "Base policy".to_string(),
                },
                cloud_value,
                raw_cloud_toml.to_string(),
                cloud_base_dir,
            )],
            ConfigRequirements::default(),
            ConfigRequirementsToml::default(),
        )
        .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        assert!(rendered.contains("enterprise-managed (Base policy, cfg_123) (enabled)"));
        assert!(rendered.contains("Enterprise-managed config value:"));
        assert!(!rendered.contains("MDM value:"));
        assert!(rendered.contains("# managed by cloud"));
        assert!(rendered.contains("model = \"enterprise_model\""));
        assert!(rendered.contains("approval_policy = \"never\""));
    }

    #[test]
    fn debug_config_output_normalizes_empty_web_search_mode_list() {
        let requirements = ConfigRequirements {
            web_search_mode: ConstrainedWithSource::new(
                Constrained::allow_any(WebSearchMode::Disabled),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            ),
            ..ConfigRequirements::default()
        };

        let requirements_toml = ConfigRequirementsToml {
            allowed_approval_policies: None,
            allowed_approvals_reviewers: None,
            allowed_sandbox_modes: None,
            allowed_permission_profiles: None,
            default_permissions: None,
            remote_sandbox_config: None,
            allowed_web_search_modes: Some(Vec::new()),
            allow_managed_hooks_only: None,
            allow_appshots: None,
            allow_remote_control: None,
            computer_use: None,
            windows: None,
            guardian_policy_config: None,
            feature_requirements: None,
            hooks: None,
            mcp_servers: None,
            plugins: None,
            apps: None,
            rules: None,
            enforce_residency: None,
            network: None,
            permissions: None,
        };

        let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
            .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
        assert!(rendered.contains(&format!(
            "allowed_web_search_modes: disabled (source: {requirements_source})"
        )));
    }

    #[test]
    fn debug_config_output_lists_managed_hooks_requirement() {
        let requirements = ConfigRequirements {
            managed_hooks: Some(ConstrainedWithSource::new(
                Constrained::allow_any(ManagedHooksRequirementsToml {
                    managed_dir: Some(if cfg!(windows) {
                        std::path::PathBuf::from(r"C:\enterprise\hooks")
                    } else {
                        std::path::PathBuf::from("/enterprise/hooks")
                    }),
                    windows_managed_dir: Some(std::path::PathBuf::from(r"C:\enterprise\hooks")),
                    hooks: HookEventsToml {
                        pre_tool_use: vec![MatcherGroup {
                            matcher: Some("^Bash$".to_string()),
                            hooks: vec![HookHandlerConfig::Command {
                                command: "python3 /enterprise/hooks/pre.py".to_string(),
                                command_windows: None,
                                timeout_sec: Some(10),
                                r#async: false,
                                status_message: Some("checking".to_string()),
                            }],
                        }],
                        ..Default::default()
                    },
                }),
                Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
            )),
            ..ConfigRequirements::default()
        };
        let requirements_toml = ConfigRequirementsToml {
            hooks: requirements
                .managed_hooks
                .as_ref()
                .map(|hooks| hooks.get().clone()),
            ..ConfigRequirementsToml::default()
        };
        let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
            .expect("config layer stack");

        let rendered = render_stack_to_text(&stack);
        let requirements_source = (RequirementSource::LegacyManagedConfigTomlFromMdm).to_string();
        assert!(rendered.contains("hooks:"));
        assert!(rendered.contains("handlers=1"));
        assert!(rendered.contains(&format!("(source: {requirements_source})")));
    }

    #[test]
    fn session_all_proxy_url_uses_socks_when_enabled() {
        assert_eq!(
            session_all_proxy_url(
                "127.0.0.1:3128",
                "127.0.0.1:8081",
                /*socks_enabled*/ true
            ),
            "socks5h://127.0.0.1:8081".to_string()
        );
    }

    #[test]
    fn session_all_proxy_url_uses_http_when_socks_disabled() {
        assert_eq!(
            session_all_proxy_url(
                "127.0.0.1:3128",
                "127.0.0.1:8081",
                /*socks_enabled*/ false
            ),
            "http://127.0.0.1:3128".to_string()
        );
    }
}
