use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use serde::de::Error as _;
use serde::de::value::Error as ValueDeserializerError;
use serde::de::value::StrDeserializer;
use std::collections::BTreeMap;
use std::fmt;
use wildmatch::WildMatchPattern;

use super::requirements_exec_policy::RequirementsExecPolicy;
use super::requirements_exec_policy::RequirementsExecPolicyToml;
use crate::Constrained;
use crate::ConstraintError;
use crate::ManagedHooksRequirementsToml;
use crate::mcp_types::AppToolApproval;
use crate::permissions_toml::PermissionProfileToml;
use crate::types::WindowsSandboxModeToml;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequirementSource {
    Unknown,
    MdmManagedPreferences {
        domain: String,
        key: String,
    },
    /// Multiple requirements layers contributed to the final value. Sources are
    /// stored highest-priority first, matching the order surfaced in errors.
    Composite {
        sources: Vec<RequirementSource>,
    },
    /// A backend-delivered enterprise-managed layer. `id` is the stable backend
    /// identifier; `name` is the admin-facing display name.
    EnterpriseManaged {
        id: String,
        name: String,
    },
    SystemRequirementsToml {
        file: AbsolutePathBuf,
    },
    LegacyManagedConfigTomlFromFile {
        file: AbsolutePathBuf,
    },
    LegacyManagedConfigTomlFromMdm,
}

impl RequirementSource {
    pub fn composite(sources: impl IntoIterator<Item = RequirementSource>) -> Self {
        let mut flattened = Vec::new();
        for source in sources {
            source.append_to_composite(&mut flattened);
        }

        match flattened.len() {
            0 => RequirementSource::Unknown,
            1 => flattened.remove(0),
            _ => RequirementSource::Composite { sources: flattened },
        }
    }

    fn append_to_composite(self, flattened: &mut Vec<RequirementSource>) {
        match self {
            RequirementSource::Composite { sources } => {
                for source in sources {
                    source.append_to_composite(flattened);
                }
            }
            source => {
                if !flattened.contains(&source) {
                    flattened.push(source);
                }
            }
        }
    }
}

impl fmt::Display for RequirementSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RequirementSource::Unknown => write!(f, "<unspecified>"),
            RequirementSource::MdmManagedPreferences { domain, key } => {
                write!(f, "MDM {domain}:{key}")
            }
            RequirementSource::Composite { sources } => {
                write!(f, "requirements layers: ")?;
                for (index, source) in sources.iter().enumerate() {
                    if index > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{source}")?;
                }
                Ok(())
            }
            RequirementSource::EnterpriseManaged { id, name } => {
                write!(f, "enterprise-managed requirements {name} ({id})")
            }
            RequirementSource::SystemRequirementsToml { file } => {
                write!(f, "{}", file.as_path().display())
            }
            RequirementSource::LegacyManagedConfigTomlFromFile { file } => {
                write!(f, "{}", file.as_path().display())
            }
            RequirementSource::LegacyManagedConfigTomlFromMdm => {
                write!(f, "MDM managed_config.toml (legacy)")
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ConstrainedWithSource<T> {
    pub value: Constrained<T>,
    pub source: Option<RequirementSource>,
}

impl<T> ConstrainedWithSource<T> {
    pub fn new(value: Constrained<T>, source: Option<RequirementSource>) -> Self {
        Self { value, source }
    }
}

impl<T> std::ops::Deref for ConstrainedWithSource<T> {
    type Target = Constrained<T>;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

/// Normalized version of [`ConfigRequirementsToml`] after deserialization and
/// normalization.
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigRequirements {
    pub approval_policy: ConstrainedWithSource<AskForApproval>,
    pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
    pub permission_profile: ConstrainedWithSource<PermissionProfile>,
    pub windows_sandbox_mode: ConstrainedWithSource<Option<WindowsSandboxModeToml>>,
    pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
    pub allow_managed_hooks_only: Option<Sourced<bool>>,
    pub allow_appshots: Option<Sourced<bool>>,
    pub allow_remote_control: Option<Sourced<bool>>,
    pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
    pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
    pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
    pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
    pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
    pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
    pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
    /// Managed network constraints derived from requirements.
    pub network: Option<Sourced<NetworkConstraints>>,
    /// Managed filesystem constraints derived from requirements.
    pub filesystem: Option<Sourced<FilesystemConstraints>>,
    /// Source for the managed guardian policy config, when one is configured.
    pub guardian_policy_config_source: Option<RequirementSource>,
}

impl Default for ConfigRequirements {
    fn default() -> Self {
        Self {
            approval_policy: ConstrainedWithSource::new(
                Constrained::allow_any_from_default(),
                /*source*/ None,
            ),
            approvals_reviewer: ConstrainedWithSource::new(
                Constrained::allow_any_from_default(),
                /*source*/ None,
            ),
            permission_profile: ConstrainedWithSource::new(
                Constrained::allow_any(PermissionProfile::read_only()),
                /*source*/ None,
            ),
            windows_sandbox_mode: ConstrainedWithSource::new(
                Constrained::allow_any(/*initial_value*/ None),
                /*source*/ None,
            ),
            web_search_mode: ConstrainedWithSource::new(
                Constrained::allow_any(WebSearchMode::Cached),
                /*source*/ None,
            ),
            allow_managed_hooks_only: None,
            allow_appshots: None,
            allow_remote_control: None,
            computer_use: None,
            feature_requirements: None,
            managed_hooks: None,
            mcp_servers: None,
            plugins: None,
            exec_policy: None,
            enforce_residency: ConstrainedWithSource::new(
                Constrained::allow_any(/*initial_value*/ None),
                /*source*/ None,
            ),
            network: None,
            filesystem: None,
            guardian_policy_config_source: None,
        }
    }
}

impl ConfigRequirements {
    pub fn exec_policy_source(&self) -> Option<&RequirementSource> {
        self.exec_policy.as_ref().map(|policy| &policy.source)
    }
}

#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum McpServerIdentity {
    Command { command: String },
    Url { url: String },
}

#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct McpServerRequirement {
    pub identity: McpServerIdentity,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginRequirementsToml {
    pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
}

impl PluginRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.mcp_servers.as_ref().is_none_or(BTreeMap::is_empty)
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NetworkDomainPermissionsToml {
    #[serde(flatten)]
    pub entries: BTreeMap<String, NetworkDomainPermissionToml>,
}

impl NetworkDomainPermissionsToml {
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn allowed_domains(&self) -> Option<Vec<String>> {
        let allowed_domains: Vec<String> = self
            .entries
            .iter()
            .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Allow))
            .map(|(pattern, _)| pattern.clone())
            .collect();
        (!allowed_domains.is_empty()).then_some(allowed_domains)
    }

    pub fn denied_domains(&self) -> Option<Vec<String>> {
        let denied_domains: Vec<String> = self
            .entries
            .iter()
            .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Deny))
            .map(|(pattern, _)| pattern.clone())
            .collect();
        (!denied_domains.is_empty()).then_some(denied_domains)
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum NetworkDomainPermissionToml {
    Allow,
    Deny,
}

impl std::fmt::Display for NetworkDomainPermissionToml {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let permission = match self {
            Self::Allow => "allow",
            Self::Deny => "deny",
        };
        f.write_str(permission)
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NetworkUnixSocketPermissionsToml {
    #[serde(flatten)]
    pub entries: BTreeMap<String, NetworkUnixSocketPermissionToml>,
}

impl NetworkUnixSocketPermissionsToml {
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn allow_unix_sockets(&self) -> Vec<String> {
        self.entries
            .iter()
            .filter(|(_, permission)| matches!(permission, NetworkUnixSocketPermissionToml::Allow))
            .map(|(path, _)| path.clone())
            .collect()
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum NetworkUnixSocketPermissionToml {
    Allow,
    Deny,
}

impl std::fmt::Display for NetworkUnixSocketPermissionToml {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let permission = match self {
            Self::Allow => "allow",
            Self::Deny => "deny",
        };
        f.write_str(permission)
    }
}

#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NetworkRequirementsToml {
    pub enabled: Option<bool>,
    pub http_port: Option<u16>,
    pub socks_port: Option<u16>,
    pub allow_upstream_proxy: Option<bool>,
    pub dangerously_allow_non_loopback_proxy: Option<bool>,
    pub dangerously_allow_all_unix_sockets: Option<bool>,
    pub domains: Option<NetworkDomainPermissionsToml>,
    /// When true, only managed `allowed_domains` are respected while managed
    /// network enforcement is active. User allowlist entries are ignored.
    pub managed_allowed_domains_only: Option<bool>,
    pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
    pub allow_local_binding: Option<bool>,
}

#[derive(Deserialize)]
struct RawNetworkRequirementsToml {
    enabled: Option<bool>,
    http_port: Option<u16>,
    socks_port: Option<u16>,
    allow_upstream_proxy: Option<bool>,
    dangerously_allow_non_loopback_proxy: Option<bool>,
    dangerously_allow_all_unix_sockets: Option<bool>,
    domains: Option<NetworkDomainPermissionsToml>,
    #[serde(default)]
    allowed_domains: Option<Vec<String>>,
    /// When true, only managed `allowed_domains` are respected while managed
    /// network enforcement is active. User allowlist entries are ignored.
    managed_allowed_domains_only: Option<bool>,
    #[serde(default)]
    denied_domains: Option<Vec<String>>,
    unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
    #[serde(default)]
    allow_unix_sockets: Option<Vec<String>>,
    allow_local_binding: Option<bool>,
}

impl<'de> Deserialize<'de> for NetworkRequirementsToml {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = RawNetworkRequirementsToml::deserialize(deserializer)?;
        let RawNetworkRequirementsToml {
            enabled,
            http_port,
            socks_port,
            allow_upstream_proxy,
            dangerously_allow_non_loopback_proxy,
            dangerously_allow_all_unix_sockets,
            domains,
            allowed_domains,
            managed_allowed_domains_only,
            denied_domains,
            unix_sockets,
            allow_unix_sockets,
            allow_local_binding,
        } = raw;

        if domains.is_some() && (allowed_domains.is_some() || denied_domains.is_some()) {
            return Err(D::Error::custom(
                "`experimental_network.domains` cannot be combined with legacy `allowed_domains` or `denied_domains`",
            ));
        }

        if unix_sockets.is_some() && allow_unix_sockets.is_some() {
            return Err(D::Error::custom(
                "`experimental_network.unix_sockets` cannot be combined with legacy `allow_unix_sockets`",
            ));
        }

        Ok(Self {
            enabled,
            http_port,
            socks_port,
            allow_upstream_proxy,
            dangerously_allow_non_loopback_proxy,
            dangerously_allow_all_unix_sockets,
            domains: domains
                .or_else(|| legacy_domain_permissions_from_lists(allowed_domains, denied_domains)),
            managed_allowed_domains_only,
            unix_sockets: unix_sockets
                .or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)),
            allow_local_binding,
        })
    }
}

/// Legacy list normalization is intentionally lossy: explicit empty legacy
/// lists are treated as unset when converted to the canonical network
/// permission shape.
fn legacy_domain_permissions_from_lists(
    allowed_domains: Option<Vec<String>>,
    denied_domains: Option<Vec<String>>,
) -> Option<NetworkDomainPermissionsToml> {
    let mut entries = BTreeMap::new();

    for pattern in allowed_domains.unwrap_or_default() {
        entries.insert(pattern, NetworkDomainPermissionToml::Allow);
    }

    for pattern in denied_domains.unwrap_or_default() {
        entries.insert(pattern, NetworkDomainPermissionToml::Deny);
    }

    (!entries.is_empty()).then_some(NetworkDomainPermissionsToml { entries })
}

fn legacy_unix_socket_permissions_from_list(
    allow_unix_sockets: Option<Vec<String>>,
) -> Option<NetworkUnixSocketPermissionsToml> {
    let entries = allow_unix_sockets
        .unwrap_or_default()
        .into_iter()
        .map(|path| (path, NetworkUnixSocketPermissionToml::Allow))
        .collect::<BTreeMap<_, _>>();

    (!entries.is_empty()).then_some(NetworkUnixSocketPermissionsToml { entries })
}

/// Normalized network constraints derived from requirements TOML.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct NetworkConstraints {
    pub enabled: Option<bool>,
    pub http_port: Option<u16>,
    pub socks_port: Option<u16>,
    pub allow_upstream_proxy: Option<bool>,
    pub dangerously_allow_non_loopback_proxy: Option<bool>,
    pub dangerously_allow_all_unix_sockets: Option<bool>,
    pub domains: Option<NetworkDomainPermissionsToml>,
    /// When true, only managed `allowed_domains` are respected while managed
    /// network enforcement is active. User allowlist entries are ignored.
    pub managed_allowed_domains_only: Option<bool>,
    pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
    pub allow_local_binding: Option<bool>,
}

impl<'de> Deserialize<'de> for NetworkConstraints {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let requirements = NetworkRequirementsToml::deserialize(deserializer)?;
        Ok(requirements.into())
    }
}

impl From<NetworkRequirementsToml> for NetworkConstraints {
    fn from(value: NetworkRequirementsToml) -> Self {
        let NetworkRequirementsToml {
            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,
        } = value;
        Self {
            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,
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FilesystemRequirementsToml {
    pub deny_read: Option<Vec<FilesystemDenyReadPattern>>,
}

#[derive(Deserialize)]
struct RawFilesystemRequirementsToml {
    deny_read: Option<Vec<FilesystemDenyReadPattern>>,
    description: Option<serde::de::IgnoredAny>,
    extends: Option<serde::de::IgnoredAny>,
    workspace_roots: Option<serde::de::IgnoredAny>,
    filesystem: Option<serde::de::IgnoredAny>,
    network: Option<serde::de::IgnoredAny>,
}

impl<'de> Deserialize<'de> for FilesystemRequirementsToml {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = RawFilesystemRequirementsToml::deserialize(deserializer)?;
        let RawFilesystemRequirementsToml {
            deny_read,
            description,
            extends,
            workspace_roots,
            filesystem,
            network,
        } = raw;

        if description.is_some()
            || extends.is_some()
            || workspace_roots.is_some()
            || filesystem.is_some()
            || network.is_some()
        {
            return Err(D::Error::custom(
                "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile",
            ));
        }

        Ok(Self { deny_read })
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct PermissionsRequirementsToml {
    pub filesystem: Option<FilesystemRequirementsToml>,
    // For legacy reasons, `filesystem` stays reserved for requirements-level
    // filesystem constraints and cannot name a profile.
    #[serde(default, flatten)]
    pub profiles: BTreeMap<String, PermissionProfileToml>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FilesystemConstraints {
    pub deny_read: Vec<FilesystemDenyReadPattern>,
}

impl From<PermissionsRequirementsToml> for FilesystemConstraints {
    fn from(value: PermissionsRequirementsToml) -> Self {
        let deny_read = value
            .filesystem
            .and_then(|filesystem| filesystem.deny_read)
            .unwrap_or_default();
        Self { deny_read }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct FilesystemDenyReadPattern(String);

impl FilesystemDenyReadPattern {
    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn contains_glob(&self) -> bool {
        self.0.chars().any(is_glob_metacharacter)
    }

    pub fn from_input(input: &str) -> Result<Self, String> {
        if !input.chars().any(is_glob_metacharacter) {
            let path = deserialize_absolute_path(input)?;
            return Ok(Self(path.to_string_lossy().into_owned()));
        }

        let (directory_prefix, suffix) = split_glob_pattern(input);
        let normalized_prefix = if directory_prefix.is_empty() {
            deserialize_absolute_path(".")?
        } else {
            deserialize_absolute_path(directory_prefix)?
        };
        let normalized_prefix = normalized_prefix.to_string_lossy();
        let normalized = if suffix.is_empty() {
            normalized_prefix.into_owned()
        } else if normalized_prefix == "/" {
            format!("/{suffix}")
        } else {
            format!("{normalized_prefix}/{suffix}")
        };
        Ok(Self(normalized))
    }
}

impl From<AbsolutePathBuf> for FilesystemDenyReadPattern {
    fn from(value: AbsolutePathBuf) -> Self {
        Self(value.to_string_lossy().into_owned())
    }
}

impl<'de> Deserialize<'de> for FilesystemDenyReadPattern {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let input = String::deserialize(deserializer)?;
        Self::from_input(&input).map_err(D::Error::custom)
    }
}

fn deserialize_absolute_path(input: &str) -> Result<AbsolutePathBuf, String> {
    AbsolutePathBuf::deserialize(StrDeserializer::<ValueDeserializerError>::new(input))
        .map_err(|err| err.to_string())
}

fn split_glob_pattern(input: &str) -> (&str, &str) {
    let Some(first_glob) = input.find(is_glob_metacharacter) else {
        return ("", input);
    };
    let separator_index = input[..first_glob]
        .char_indices()
        .rev()
        .find(|(_, ch)| is_path_separator(*ch))
        .map(|(index, _)| index);

    match separator_index {
        Some(0) => ("/", &input[1..]),
        Some(index)
            if cfg!(windows)
                && index == 2
                && input.as_bytes().get(1) == Some(&b':')
                && input.as_bytes().get(2).is_some() =>
        {
            (&input[..=index], &input[index + 1..])
        }
        Some(index) => (&input[..index], &input[index + 1..]),
        None => ("", input),
    }
}

fn is_path_separator(ch: char) -> bool {
    if cfg!(windows) {
        ch == '/' || ch == '\\'
    } else {
        ch == '/'
    }
}

fn is_glob_metacharacter(ch: char) -> bool {
    matches!(ch, '*' | '?' | '[')
}

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
pub enum WebSearchModeRequirement {
    Disabled,
    Cached,
    Live,
}

impl From<WebSearchMode> for WebSearchModeRequirement {
    fn from(mode: WebSearchMode) -> Self {
        match mode {
            WebSearchMode::Disabled => WebSearchModeRequirement::Disabled,
            WebSearchMode::Cached => WebSearchModeRequirement::Cached,
            WebSearchMode::Live => WebSearchModeRequirement::Live,
        }
    }
}

impl From<WebSearchModeRequirement> for WebSearchMode {
    fn from(mode: WebSearchModeRequirement) -> Self {
        match mode {
            WebSearchModeRequirement::Disabled => WebSearchMode::Disabled,
            WebSearchModeRequirement::Cached => WebSearchMode::Cached,
            WebSearchModeRequirement::Live => WebSearchMode::Live,
        }
    }
}

impl fmt::Display for WebSearchModeRequirement {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            WebSearchModeRequirement::Disabled => write!(f, "disabled"),
            WebSearchModeRequirement::Cached => write!(f, "cached"),
            WebSearchModeRequirement::Live => write!(f, "live"),
        }
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct ComputerUseRequirementsToml {
    pub allow_locked_computer_use: Option<bool>,
}

impl ComputerUseRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.allow_locked_computer_use.is_none()
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct WindowsRequirementsToml {
    pub allowed_sandbox_implementations: Option<Vec<WindowsSandboxModeToml>>,
}

impl WindowsRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.allowed_sandbox_implementations.is_none()
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct FeatureRequirementsToml {
    #[serde(flatten)]
    pub entries: BTreeMap<String, bool>,
}

impl FeatureRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppToolRequirementToml {
    pub approval_mode: Option<AppToolApproval>,
}

impl AppToolRequirementToml {
    pub fn is_empty(&self) -> bool {
        self.approval_mode.is_none()
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppToolsRequirementsToml {
    #[serde(default, flatten)]
    pub tools: BTreeMap<String, AppToolRequirementToml>,
}

impl AppToolsRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.tools.values().all(AppToolRequirementToml::is_empty)
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppRequirementToml {
    pub enabled: Option<bool>,
    pub tools: Option<AppToolsRequirementsToml>,
}

impl AppRequirementToml {
    pub fn is_empty(&self) -> bool {
        self.enabled.is_none()
            && self
                .tools
                .as_ref()
                .is_none_or(AppToolsRequirementsToml::is_empty)
    }
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppsRequirementsToml {
    #[serde(default, flatten)]
    pub apps: BTreeMap<String, AppRequirementToml>,
}

impl AppsRequirementsToml {
    pub fn is_empty(&self) -> bool {
        self.apps.values().all(AppRequirementToml::is_empty)
    }
}

/// Merge app requirements from a lower-precedence source into an existing higher-precedence set.
/// This lets managed sources (for example Cloud/MDM) enforce setting disablement across layers,
/// while exact tool approval settings keep the higher-precedence value when present.
pub(crate) fn merge_app_requirements_descending(
    base: &mut AppsRequirementsToml,
    incoming: AppsRequirementsToml,
) {
    for (app_id, incoming_requirement) in incoming.apps {
        let base_requirement = base.apps.entry(app_id).or_default();
        let higher_precedence = base_requirement.enabled;
        let lower_precedence = incoming_requirement.enabled;
        base_requirement.enabled =
            if higher_precedence == Some(false) || lower_precedence == Some(false) {
                Some(false)
            } else {
                higher_precedence.or(lower_precedence)
            };

        let Some(incoming_tools) = incoming_requirement.tools else {
            continue;
        };
        let base_tools = base_requirement.tools.get_or_insert_with(Default::default);
        for (tool_name, incoming_tool) in incoming_tools.tools {
            let base_tool = base_tools.tools.entry(tool_name).or_default();
            if base_tool.approval_mode.is_none() {
                base_tool.approval_mode = incoming_tool.approval_mode;
            }
        }
    }
}

/// Base config deserialized from system `requirements.toml` or MDM.
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsToml {
    pub allowed_approval_policies: Option<Vec<AskForApproval>>,
    pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
    pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
    pub allowed_permission_profiles: Option<BTreeMap<String, bool>>,
    pub default_permissions: Option<String>,
    pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
    pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
    pub allow_managed_hooks_only: Option<bool>,
    pub allow_appshots: Option<bool>,
    pub allow_remote_control: Option<bool>,
    pub computer_use: Option<ComputerUseRequirementsToml>,
    pub windows: Option<WindowsRequirementsToml>,
    #[serde(rename = "features", alias = "feature_requirements")]
    pub feature_requirements: Option<FeatureRequirementsToml>,
    pub hooks: Option<ManagedHooksRequirementsToml>,
    pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
    pub plugins: Option<BTreeMap<String, PluginRequirementsToml>>,
    pub apps: Option<AppsRequirementsToml>,
    pub rules: Option<RequirementsExecPolicyToml>,
    pub enforce_residency: Option<ResidencyRequirement>,
    #[serde(rename = "experimental_network")]
    pub network: Option<NetworkRequirementsToml>,
    pub permissions: Option<PermissionsRequirementsToml>,
    pub guardian_policy_config: Option<String>,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct RemoteSandboxConfigToml {
    pub hostname_patterns: Vec<String>,
    pub allowed_sandbox_modes: Vec<SandboxModeRequirement>,
}

/// Value paired with the requirement source it came from, for better error
/// messages.
#[derive(Debug, Clone, PartialEq)]
pub struct Sourced<T> {
    pub value: T,
    pub source: RequirementSource,
}

impl<T> Sourced<T> {
    pub fn new(value: T, source: RequirementSource) -> Self {
        Self { value, source }
    }
}

impl<T> std::ops::Deref for Sourced<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsWithSources {
    pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
    pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
    pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
    pub allowed_permission_profiles: Option<Sourced<BTreeMap<String, bool>>>,
    pub default_permissions: Option<Sourced<String>>,
    pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
    pub allow_managed_hooks_only: Option<Sourced<bool>>,
    pub allow_appshots: Option<Sourced<bool>>,
    pub allow_remote_control: Option<Sourced<bool>>,
    pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
    pub windows: Option<Sourced<WindowsRequirementsToml>>,
    pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
    pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
    pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
    pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
    pub apps: Option<Sourced<AppsRequirementsToml>>,
    pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
    pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
    pub network: Option<Sourced<NetworkRequirementsToml>>,
    pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
    pub guardian_policy_config: Option<Sourced<String>>,
}

impl ConfigRequirementsWithSources {
    pub fn merge_unset_fields(&mut self, source: RequirementSource, other: ConfigRequirementsToml) {
        // For every field in `other` that is `Some`, if the corresponding field
        // in `self` is `None`, copy the value from `other` into `self`.
        macro_rules! fill_missing_take {
            ($base:expr, $other:expr, $source:expr, { $($field:ident),+ $(,)? }) => {
                $(
                    if $base.$field.is_none()
                        && let Some(value) = $other.$field.take()
                    {
                        $base.$field = Some(Sourced::new(value, $source.clone()));
                    }
                )+
            };
        }

        // Destructure without `..` so adding fields to `ConfigRequirementsToml`
        // forces this merge logic to be updated.
        let ConfigRequirementsToml {
            allowed_approval_policies: _,
            allowed_approvals_reviewers: _,
            allowed_sandbox_modes: _,
            allowed_permission_profiles: _,
            default_permissions: _,
            remote_sandbox_config: _,
            allowed_web_search_modes: _,
            allow_managed_hooks_only: _,
            allow_appshots: _,
            allow_remote_control: _,
            computer_use: _,
            windows: _,
            feature_requirements: _,
            hooks: _,
            mcp_servers: _,
            plugins: _,
            apps: _,
            rules: _,
            enforce_residency: _,
            network: _,
            permissions: _,
            guardian_policy_config: _,
        } = &other;

        let mut other = other;
        if other
            .guardian_policy_config
            .as_deref()
            .is_some_and(|value| value.trim().is_empty())
        {
            other.guardian_policy_config = None;
        }
        fill_missing_take!(
            self,
            other,
            source,
            {
                allowed_approval_policies,
                allowed_approvals_reviewers,
                allowed_sandbox_modes,
                allowed_permission_profiles,
                default_permissions,
                allowed_web_search_modes,
                allow_managed_hooks_only,
                allow_appshots,
                allow_remote_control,
                computer_use,
                windows,
                feature_requirements,
                hooks,
                mcp_servers,
                plugins,
                rules,
                enforce_residency,
                network,
                permissions,
                guardian_policy_config,
            }
        );

        if let Some(incoming_apps) = other.apps.take() {
            if let Some(existing_apps) = self.apps.as_mut() {
                merge_app_requirements_descending(&mut existing_apps.value, incoming_apps);
            } else {
                self.apps = Some(Sourced::new(incoming_apps, source));
            }
        }
    }

    pub fn into_toml(self) -> ConfigRequirementsToml {
        let ConfigRequirementsWithSources {
            allowed_approval_policies,
            allowed_approvals_reviewers,
            allowed_sandbox_modes,
            allowed_permission_profiles,
            default_permissions,
            allowed_web_search_modes,
            allow_managed_hooks_only,
            allow_appshots,
            allow_remote_control,
            computer_use,
            windows,
            feature_requirements,
            hooks,
            mcp_servers,
            plugins,
            apps,
            rules,
            enforce_residency,
            network,
            permissions,
            guardian_policy_config,
        } = self;
        ConfigRequirementsToml {
            allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
            allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value),
            allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
            allowed_permission_profiles: allowed_permission_profiles.map(|sourced| sourced.value),
            default_permissions: default_permissions.map(|sourced| sourced.value),
            remote_sandbox_config: None,
            allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
            allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
            allow_appshots: allow_appshots.map(|sourced| sourced.value),
            allow_remote_control: allow_remote_control.map(|sourced| sourced.value),
            computer_use: computer_use.map(|sourced| sourced.value),
            windows: windows.map(|sourced| sourced.value),
            feature_requirements: feature_requirements.map(|sourced| sourced.value),
            hooks: hooks.map(|sourced| sourced.value),
            mcp_servers: mcp_servers.map(|sourced| sourced.value),
            plugins: plugins.map(|sourced| sourced.value),
            apps: apps.map(|sourced| sourced.value),
            rules: rules.map(|sourced| sourced.value),
            enforce_residency: enforce_residency.map(|sourced| sourced.value),
            network: network.map(|sourced| sourced.value),
            permissions: permissions.map(|sourced| sourced.value),
            guardian_policy_config: guardian_policy_config.map(|sourced| sourced.value),
        }
    }
}

fn normalize_hostname(hostname: &str) -> Option<String> {
    let hostname = hostname.trim().trim_end_matches('.');
    (!hostname.is_empty()).then(|| hostname.to_ascii_lowercase())
}

fn hostname_matches_any_pattern(hostname: &str, patterns: &[String]) -> bool {
    patterns.iter().any(|pattern| {
        normalize_hostname(pattern)
            .map(|pattern| WildMatchPattern::<'*', '?'>::new_case_insensitive(&pattern))
            .is_some_and(|pattern| pattern.matches(hostname))
    })
}

/// Currently, `external-sandbox` is not supported in config.toml, but it is
/// supported through programmatic use.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum SandboxModeRequirement {
    #[serde(rename = "read-only")]
    ReadOnly,

    #[serde(rename = "workspace-write")]
    WorkspaceWrite,

    #[serde(rename = "danger-full-access")]
    DangerFullAccess,

    #[serde(rename = "external-sandbox")]
    ExternalSandbox,
}

impl From<SandboxMode> for SandboxModeRequirement {
    fn from(mode: SandboxMode) -> Self {
        match mode {
            SandboxMode::ReadOnly => SandboxModeRequirement::ReadOnly,
            SandboxMode::WorkspaceWrite => SandboxModeRequirement::WorkspaceWrite,
            SandboxMode::DangerFullAccess => SandboxModeRequirement::DangerFullAccess,
        }
    }
}

#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ResidencyRequirement {
    Us,
}

impl ConfigRequirementsToml {
    pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) {
        let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else {
            return;
        };
        let Some(hostname) = hostname.and_then(normalize_hostname) else {
            return;
        };
        let Some(matched_config) = remote_sandbox_config
            .iter()
            .find(|config| hostname_matches_any_pattern(&hostname, &config.hostname_patterns))
        else {
            return;
        };
        self.allowed_sandbox_modes = Some(matched_config.allowed_sandbox_modes.clone());
    }

    pub fn is_empty(&self) -> bool {
        self.allowed_approval_policies.is_none()
            && self.allowed_approvals_reviewers.is_none()
            && self.allowed_sandbox_modes.is_none()
            && self.allowed_permission_profiles.is_none()
            && self.default_permissions.is_none()
            && self.remote_sandbox_config.is_none()
            && self.allowed_web_search_modes.is_none()
            && self.allow_managed_hooks_only.is_none()
            && self.allow_appshots.is_none()
            && self.allow_remote_control.is_none()
            && self
                .computer_use
                .as_ref()
                .is_none_or(ComputerUseRequirementsToml::is_empty)
            && self
                .windows
                .as_ref()
                .is_none_or(WindowsRequirementsToml::is_empty)
            && self
                .feature_requirements
                .as_ref()
                .is_none_or(FeatureRequirementsToml::is_empty)
            && self
                .hooks
                .as_ref()
                .is_none_or(ManagedHooksRequirementsToml::is_empty)
            && self.mcp_servers.is_none()
            && self
                .plugins
                .as_ref()
                .is_none_or(|plugins| plugins.values().all(PluginRequirementsToml::is_empty))
            && self
                .apps
                .as_ref()
                .is_none_or(AppsRequirementsToml::is_empty)
            && self.rules.is_none()
            && self.enforce_residency.is_none()
            && self.network.is_none()
            && self.permissions.is_none()
            && self
                .guardian_policy_config
                .as_deref()
                .is_none_or(|value| value.trim().is_empty())
    }
}

impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
    type Error = ConstraintError;

    fn try_from(toml: ConfigRequirementsWithSources) -> Result<Self, Self::Error> {
        // Profile catalog selection remains on ConfigRequirementsToml for
        // config loading and requirements API projection. The normalized
        // constraints below only need the compiled PermissionProfile envelope.
        let ConfigRequirementsWithSources {
            allowed_approval_policies,
            allowed_approvals_reviewers,
            allowed_sandbox_modes,
            allowed_permission_profiles: _,
            default_permissions: _,
            allowed_web_search_modes,
            allow_managed_hooks_only,
            allow_appshots,
            allow_remote_control,
            computer_use,
            windows,
            feature_requirements,
            hooks,
            mcp_servers,
            plugins,
            apps: _apps,
            rules,
            enforce_residency,
            network,
            permissions,
            guardian_policy_config,
        } = toml;

        let approval_policy = match allowed_approval_policies {
            Some(Sourced {
                value: policies,
                source: requirement_source,
            }) => {
                let Some(initial_value) = policies.first().copied() else {
                    return Err(ConstraintError::empty_field("allowed_approval_policies"));
                };

                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(initial_value, move |candidate| {
                    if policies.contains(candidate) {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "approval_policy",
                            candidate: format!("{candidate:?}"),
                            allowed: format!("{policies:?}"),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            None => ConstrainedWithSource::new(
                Constrained::allow_any_from_default(),
                /*source*/ None,
            ),
        };

        let approvals_reviewer = match allowed_approvals_reviewers {
            Some(Sourced {
                value: reviewers,
                source: requirement_source,
            }) => {
                let Some(initial_value) = reviewers.first().copied() else {
                    return Err(ConstraintError::empty_field("allowed_approvals_reviewers"));
                };

                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(initial_value, move |candidate| {
                    if reviewers.contains(candidate) {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "approvals_reviewer",
                            candidate: format!("{candidate:?}"),
                            allowed: format!("{reviewers:?}"),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            None => ConstrainedWithSource::new(
                Constrained::allow_any_from_default(),
                /*source*/ None,
            ),
        };

        let default_permission_profile = PermissionProfile::read_only();
        let permission_profile = match allowed_sandbox_modes {
            Some(Sourced {
                value: modes,
                source: requirement_source,
            }) => {
                if !modes.contains(&SandboxModeRequirement::ReadOnly) {
                    return Err(ConstraintError::InvalidValue {
                        field_name: "allowed_sandbox_modes",
                        candidate: format!("{modes:?}"),
                        allowed: "must include 'read-only' to allow any PermissionProfile"
                            .to_string(),
                        requirement_source,
                    });
                };

                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(default_permission_profile, move |candidate| {
                    let mode = sandbox_mode_requirement_for_permission_profile(candidate);
                    if modes.contains(&mode) {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "sandbox_mode",
                            candidate: format!("{mode:?}"),
                            allowed: format!("{modes:?}"),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            None => ConstrainedWithSource::new(
                Constrained::allow_any(default_permission_profile),
                /*source*/ None,
            ),
        };
        let windows_sandbox_mode = match windows {
            Some(Sourced {
                value:
                    WindowsRequirementsToml {
                        allowed_sandbox_implementations: Some(implementations),
                    },
                source: requirement_source,
            }) => {
                if implementations.is_empty() {
                    return Err(ConstraintError::empty_field(
                        "windows.allowed_sandbox_implementations",
                    ));
                }
                // Prefer elevated when both Windows sandbox implementations are allowed.
                let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) {
                    WindowsSandboxModeToml::Elevated
                } else {
                    WindowsSandboxModeToml::Unelevated
                };

                let requirement_source_for_error = requirement_source.clone();
                let constrained =
                    Constrained::new(Some(initial_value), move |candidate| match candidate {
                        Some(candidate) if implementations.contains(candidate) => Ok(()),
                        _ => Err(ConstraintError::InvalidValue {
                            field_name: "windows.sandbox",
                            candidate: format!("{candidate:?}"),
                            allowed: format!("{implementations:?}"),
                            requirement_source: requirement_source_for_error.clone(),
                        }),
                    })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            Some(_) | None => ConstrainedWithSource::new(
                Constrained::allow_any(/*initial_value*/ None),
                /*source*/ None,
            ),
        };
        let exec_policy = match rules {
            Some(Sourced { value, source }) => {
                let policy = value.to_requirements_policy().map_err(|err| {
                    ConstraintError::ExecPolicyParse {
                        requirement_source: source.clone(),
                        reason: err.to_string(),
                    }
                })?;
                Some(Sourced::new(policy, source))
            }
            None => None,
        };
        let web_search_mode = match allowed_web_search_modes {
            Some(Sourced {
                value: modes,
                source: requirement_source,
            }) => {
                let mut accepted = modes.into_iter().collect::<std::collections::BTreeSet<_>>();
                accepted.insert(WebSearchModeRequirement::Disabled);
                let allowed_for_error = format!(
                    "{:?}",
                    accepted
                        .iter()
                        .copied()
                        .map(WebSearchMode::from)
                        .collect::<Vec<_>>()
                );

                let initial_value = if accepted.contains(&WebSearchModeRequirement::Cached) {
                    WebSearchMode::Cached
                } else if accepted.contains(&WebSearchModeRequirement::Live) {
                    WebSearchMode::Live
                } else {
                    WebSearchMode::Disabled
                };
                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(initial_value, move |candidate| {
                    if accepted.contains(&(*candidate).into()) {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "web_search_mode",
                            candidate: format!("{candidate:?}"),
                            allowed: allowed_for_error.clone(),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            None => ConstrainedWithSource::new(
                Constrained::allow_any(WebSearchMode::Cached),
                /*source*/ None,
            ),
        };
        let feature_requirements =
            feature_requirements.filter(|requirements| !requirements.value.is_empty());
        let managed_hooks = hooks
            .filter(|managed_hooks| managed_hooks.value.handler_count() > 0)
            .map(|sourced_hooks| {
                let Sourced {
                    value,
                    source: requirement_source,
                } = sourced_hooks;
                let allowed = value;
                let allowed_for_error = format!("{allowed:?}");
                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(allowed.clone(), move |candidate| {
                    if candidate == &allowed {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "hooks",
                            candidate: format!("{candidate:?}"),
                            allowed: allowed_for_error.clone(),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                Ok(ConstrainedWithSource::new(
                    constrained,
                    Some(requirement_source),
                ))
            })
            .transpose()?;

        let enforce_residency = match enforce_residency {
            Some(Sourced {
                value: residency,
                source: requirement_source,
            }) => {
                let required = Some(residency);
                let requirement_source_for_error = requirement_source.clone();
                let constrained = Constrained::new(required, move |candidate| {
                    if candidate == &required {
                        Ok(())
                    } else {
                        Err(ConstraintError::InvalidValue {
                            field_name: "enforce_residency",
                            candidate: format!("{candidate:?}"),
                            allowed: format!("{required:?}"),
                            requirement_source: requirement_source_for_error.clone(),
                        })
                    }
                })?;
                ConstrainedWithSource::new(constrained, Some(requirement_source))
            }
            None => ConstrainedWithSource::new(
                Constrained::allow_any(/*initial_value*/ None),
                /*source*/ None,
            ),
        };
        let network = network.map(|sourced_network| {
            let Sourced { value, source } = sourced_network;
            Sourced::new(NetworkConstraints::from(value), source)
        });
        let filesystem = permissions.map(|sourced_permissions| {
            let Sourced { value, source } = sourced_permissions;
            Sourced::new(FilesystemConstraints::from(value), source)
        });
        let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
        Ok(ConfigRequirements {
            approval_policy,
            approvals_reviewer,
            permission_profile,
            windows_sandbox_mode,
            web_search_mode,
            allow_managed_hooks_only,
            allow_appshots,
            allow_remote_control,
            computer_use,
            feature_requirements,
            managed_hooks,
            mcp_servers,
            plugins,
            exec_policy,
            enforce_residency,
            network,
            filesystem,
            guardian_policy_config_source,
        })
    }
}

pub fn sandbox_mode_requirement_for_permission_profile(
    permission_profile: &PermissionProfile,
) -> SandboxModeRequirement {
    match permission_profile {
        PermissionProfile::Disabled => SandboxModeRequirement::DangerFullAccess,
        PermissionProfile::External { .. } => SandboxModeRequirement::ExternalSandbox,
        PermissionProfile::Managed { .. } => {
            let file_system_policy = permission_profile.file_system_sandbox_policy();
            if file_system_policy.has_full_disk_write_access() {
                SandboxModeRequirement::DangerFullAccess
            } else if file_system_policy
                .entries
                .iter()
                .any(|entry| entry.access.can_write())
            {
                SandboxModeRequirement::WorkspaceWrite
            } else {
                SandboxModeRequirement::ReadOnly
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::HookEventsToml;
    use anyhow::Result;
    use codex_execpolicy::Decision;
    use codex_execpolicy::Evaluation;
    use codex_execpolicy::RuleMatch;
    use codex_protocol::permissions::NetworkSandboxPolicy;
    use codex_utils_absolute_path::AbsolutePathBuf;
    use codex_utils_absolute_path::AbsolutePathBufGuard;
    use pretty_assertions::assert_eq;
    use toml::from_str;

    fn tokens(cmd: &[&str]) -> Vec<String> {
        cmd.iter().map(std::string::ToString::to_string).collect()
    }

    fn system_requirements_toml_file_for_test() -> Result<AbsolutePathBuf> {
        Ok(AbsolutePathBuf::try_from(
            std::env::temp_dir().join("requirements.toml"),
        )?)
    }

    #[test]
    fn composite_requirement_source_flattens_and_deduplicates_sources() {
        let mdm_source = RequirementSource::MdmManagedPreferences {
            domain: "com.openai.codex".to_string(),
            key: "requirements_toml_base64".to_string(),
        };
        let legacy_source = RequirementSource::LegacyManagedConfigTomlFromMdm;

        assert_eq!(
            RequirementSource::composite([
                mdm_source.clone(),
                RequirementSource::composite([legacy_source.clone(), mdm_source.clone()]),
            ]),
            RequirementSource::Composite {
                sources: vec![mdm_source, legacy_source],
            }
        );
    }

    fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
        let ConfigRequirementsToml {
            allowed_approval_policies,
            allowed_approvals_reviewers,
            allowed_sandbox_modes,
            allowed_permission_profiles,
            default_permissions,
            remote_sandbox_config: _,
            allowed_web_search_modes,
            allow_managed_hooks_only,
            allow_appshots,
            allow_remote_control,
            computer_use,
            windows,
            feature_requirements,
            hooks,
            mcp_servers,
            plugins,
            apps,
            rules,
            enforce_residency,
            network,
            permissions,
            guardian_policy_config,
        } = toml;
        ConfigRequirementsWithSources {
            allowed_approval_policies: allowed_approval_policies
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allowed_approvals_reviewers: allowed_approvals_reviewers
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allowed_sandbox_modes: allowed_sandbox_modes
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allowed_permission_profiles: allowed_permission_profiles
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            default_permissions: default_permissions
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allowed_web_search_modes: allowed_web_search_modes
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allow_managed_hooks_only: allow_managed_hooks_only
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allow_appshots: allow_appshots
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            allow_remote_control: allow_remote_control
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            feature_requirements: feature_requirements
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            plugins: plugins.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            enforce_residency: enforce_residency
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
            network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
            guardian_policy_config: guardian_policy_config
                .map(|value| Sourced::new(value, RequirementSource::Unknown)),
        }
    }

    #[test]
    fn deserialize_allow_managed_hooks_only() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                allow_managed_hooks_only = true
            "#,
        )?;

        assert_eq!(requirements.allow_managed_hooks_only, Some(true));
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn allow_managed_hooks_only_false_is_still_configured() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                allow_managed_hooks_only = false
            "#,
        )?;

        assert_eq!(requirements.allow_managed_hooks_only, Some(false));
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn deserialize_managed_permission_profiles() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                default_permissions = "managed-standard"

                [allowed_permission_profiles]
                managed-standard = true
                managed-build = true

                [permissions.managed-standard]
                extends = ":workspace"

                [permissions.managed-build]
                extends = "managed-standard"
            "#,
        )?;

        assert_eq!(
            requirements.allowed_permission_profiles,
            Some(BTreeMap::from([
                ("managed-build".to_string(), true),
                ("managed-standard".to_string(), true),
            ]))
        );
        assert_eq!(
            requirements.default_permissions,
            Some("managed-standard".to_string())
        );
        let permissions = requirements
            .permissions
            .as_ref()
            .expect("managed permission profiles");
        assert!(permissions.profiles.contains_key("managed-standard"));
        assert!(
            permissions
                .profiles
                .get("managed-build")
                .and_then(|profile| profile.extends.as_deref())
                .is_some()
        );
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn deserialize_allow_appshots() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                allow_appshots = true
            "#,
        )?;

        assert_eq!(requirements.allow_appshots, Some(true));
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn filesystem_requirements_table_cannot_define_a_permission_profile() {
        let err = from_str::<ConfigRequirementsToml>(
            r#"
                [permissions.filesystem]
                extends = ":workspace"
            "#,
        )
        .expect_err("filesystem requirements cannot define a permission profile");

        assert!(
            err.to_string().contains(
                "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile"
            ),
            "unexpected error: {err:#}"
        );
    }

    #[test]
    fn allow_appshots_false_is_still_configured() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                allow_appshots = false
            "#,
        )?;

        assert_eq!(requirements.allow_appshots, Some(false));
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn allow_remote_control_false_is_still_configured() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                allow_remote_control = false
            "#,
        )?;

        assert_eq!(requirements.allow_remote_control, Some(false));
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn deserialize_computer_use_requirements() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
                [computer_use]
                allow_locked_computer_use = false
            "#,
        )?;

        assert_eq!(
            requirements.computer_use,
            Some(ComputerUseRequirementsToml {
                allow_locked_computer_use: Some(false),
            })
        );
        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn merge_unset_fields_copies_every_field_and_sets_sources() {
        let mut target = ConfigRequirementsWithSources::default();
        let source = RequirementSource::LegacyManagedConfigTomlFromMdm;

        let allowed_approval_policies = vec![AskForApproval::UnlessTrusted, AskForApproval::Never];
        let allowed_approvals_reviewers =
            vec![ApprovalsReviewer::AutoReview, ApprovalsReviewer::User];
        let allowed_sandbox_modes = vec![
            SandboxModeRequirement::WorkspaceWrite,
            SandboxModeRequirement::DangerFullAccess,
        ];
        let allowed_web_search_modes = vec![
            WebSearchModeRequirement::Cached,
            WebSearchModeRequirement::Live,
        ];
        let feature_requirements = FeatureRequirementsToml {
            entries: BTreeMap::from([("personality".to_string(), true)]),
        };
        let computer_use = ComputerUseRequirementsToml {
            allow_locked_computer_use: Some(false),
        };
        let enforce_residency = ResidencyRequirement::Us;
        let enforce_source = source.clone();
        let guardian_policy_config = "Use the company-managed guardian policy.".to_string();

        // Intentionally constructed without `..Default::default()` so adding a new field to
        // `ConfigRequirementsToml` forces this test to be updated.
        let other = ConfigRequirementsToml {
            allowed_approval_policies: Some(allowed_approval_policies.clone()),
            allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()),
            allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
            allowed_permission_profiles: Some(BTreeMap::from([("managed".to_string(), true)])),
            default_permissions: Some("managed".to_string()),
            remote_sandbox_config: None,
            allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
            allow_managed_hooks_only: Some(true),
            allow_appshots: Some(false),
            allow_remote_control: Some(false),
            computer_use: Some(computer_use.clone()),
            windows: None,
            feature_requirements: Some(feature_requirements.clone()),
            hooks: None,
            mcp_servers: None,
            plugins: None,
            apps: None,
            rules: None,
            enforce_residency: Some(enforce_residency),
            network: None,
            permissions: None,
            guardian_policy_config: Some(guardian_policy_config.clone()),
        };

        target.merge_unset_fields(source.clone(), other);

        assert_eq!(
            target,
            ConfigRequirementsWithSources {
                allowed_approval_policies: Some(Sourced::new(
                    allowed_approval_policies,
                    source.clone()
                )),
                allowed_approvals_reviewers: Some(Sourced::new(
                    allowed_approvals_reviewers,
                    source.clone(),
                )),
                allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)),
                allowed_permission_profiles: Some(Sourced::new(
                    BTreeMap::from([("managed".to_string(), true)]),
                    source.clone(),
                )),
                default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)),
                allowed_web_search_modes: Some(Sourced::new(
                    allowed_web_search_modes,
                    enforce_source.clone(),
                )),
                allow_managed_hooks_only: Some(Sourced::new(
                    /*value*/ true,
                    enforce_source.clone(),
                )),
                allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)),
                allow_remote_control: Some(Sourced::new(
                    /*value*/ false,
                    enforce_source.clone(),
                )),
                computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
                windows: None,
                feature_requirements: Some(Sourced::new(
                    feature_requirements,
                    enforce_source.clone(),
                )),
                hooks: None,
                mcp_servers: None,
                plugins: None,
                apps: None,
                rules: None,
                enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
                network: None,
                permissions: None,
                guardian_policy_config: Some(Sourced::new(guardian_policy_config, source)),
            }
        );
    }

    #[test]
    fn merge_unset_fields_fills_missing_values() -> Result<()> {
        let source: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["on-request"]
            "#,
        )?;

        let source_location = RequirementSource::MdmManagedPreferences {
            domain: "com.codex".to_string(),
            key: "allowed_approval_policies".to_string(),
        };

        let mut empty_target = ConfigRequirementsWithSources::default();
        empty_target.merge_unset_fields(source_location.clone(), source);
        assert_eq!(
            empty_target,
            ConfigRequirementsWithSources {
                allowed_approval_policies: Some(Sourced::new(
                    vec![AskForApproval::OnRequest],
                    source_location,
                )),
                allowed_approvals_reviewers: None,
                allowed_sandbox_modes: None,
                allowed_permission_profiles: None,
                default_permissions: None,
                allowed_web_search_modes: None,
                allow_managed_hooks_only: None,
                allow_appshots: None,
                allow_remote_control: None,
                computer_use: None,
                windows: None,
                feature_requirements: None,
                hooks: None,
                mcp_servers: None,
                plugins: None,
                apps: None,
                rules: None,
                enforce_residency: None,
                network: None,
                permissions: None,
                guardian_policy_config: None,
            }
        );
        Ok(())
    }

    #[test]
    fn merge_unset_fields_does_not_overwrite_existing_values() -> Result<()> {
        let existing_source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut populated_target = ConfigRequirementsWithSources::default();
        let populated_requirements: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["never"]
            "#,
        )?;
        populated_target.merge_unset_fields(existing_source.clone(), populated_requirements);

        let source: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["on-request"]
            "#,
        )?;
        let source_location = RequirementSource::MdmManagedPreferences {
            domain: "com.codex".to_string(),
            key: "allowed_approval_policies".to_string(),
        };
        populated_target.merge_unset_fields(source_location, source);

        assert_eq!(
            populated_target,
            ConfigRequirementsWithSources {
                allowed_approval_policies: Some(Sourced::new(
                    vec![AskForApproval::Never],
                    existing_source,
                )),
                allowed_approvals_reviewers: None,
                allowed_sandbox_modes: None,
                allowed_permission_profiles: None,
                default_permissions: None,
                allowed_web_search_modes: None,
                allow_managed_hooks_only: None,
                allow_appshots: None,
                allow_remote_control: None,
                computer_use: None,
                windows: None,
                feature_requirements: None,
                hooks: None,
                mcp_servers: None,
                plugins: None,
                apps: None,
                rules: None,
                enforce_residency: None,
                network: None,
                permissions: None,
                guardian_policy_config: None,
            }
        );
        Ok(())
    }

    #[test]
    fn merge_unset_fields_ignores_blank_guardian_override() {
        let mut target = ConfigRequirementsWithSources::default();
        target.merge_unset_fields(
            RequirementSource::LegacyManagedConfigTomlFromMdm,
            ConfigRequirementsToml {
                guardian_policy_config: Some("   \n\t".to_string()),
                ..Default::default()
            },
        );
        target.merge_unset_fields(
            RequirementSource::SystemRequirementsToml {
                file: system_requirements_toml_file_for_test()
                    .expect("system requirements.toml path"),
            },
            ConfigRequirementsToml {
                guardian_policy_config: Some("Use the system guardian policy.".to_string()),
                ..Default::default()
            },
        );

        assert_eq!(
            target.guardian_policy_config,
            Some(Sourced::new(
                "Use the system guardian policy.".to_string(),
                RequirementSource::SystemRequirementsToml {
                    file: system_requirements_toml_file_for_test()
                        .expect("system requirements.toml path"),
                },
            )),
        );
    }

    #[test]
    fn deserialize_guardian_policy_config() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
guardian_policy_config = """
Use the cloud-managed guardian policy.
"""
"#,
        )?;

        assert_eq!(
            requirements.guardian_policy_config.as_deref(),
            Some("Use the cloud-managed guardian policy.\n")
        );
        Ok(())
    }

    #[test]
    fn blank_guardian_policy_config_is_empty() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
guardian_policy_config = """

"""
"#,
        )?;

        assert!(requirements.is_empty());
        Ok(())
    }

    #[test]
    fn allowed_approvals_reviewers_is_not_empty() -> Result<()> {
        let requirements: ConfigRequirementsToml = from_str(
            r#"
allowed_approvals_reviewers = ["user"]
"#,
        )?;

        assert!(!requirements.is_empty());
        Ok(())
    }

    #[test]
    fn deserialize_filesystem_deny_read_requirements() -> Result<()> {
        let deny_read_0 = if cfg!(windows) {
            r"C:\Users\alice\.gitconfig"
        } else {
            "/home/alice/.gitconfig"
        };
        let deny_read_1 = if cfg!(windows) {
            r"C:\Users\alice\.ssh"
        } else {
            "/home/alice/.ssh"
        };
        let toml_str = format!(
            r#"
            [permissions.filesystem]
            deny_read = [{deny_read_0:?}, {deny_read_1:?}]
        "#
        );

        let config: ConfigRequirementsToml = from_str(&toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.filesystem,
            Some(Sourced::new(
                FilesystemConstraints {
                    deny_read: vec![
                        AbsolutePathBuf::from_absolute_path(deny_read_0)?.into(),
                        AbsolutePathBuf::from_absolute_path(deny_read_1)?.into(),
                    ],
                },
                RequirementSource::Unknown,
            ))
        );

        Ok(())
    }

    #[test]
    fn deserialize_filesystem_deny_read_glob_requirements() -> Result<()> {
        let temp_dir = std::env::temp_dir();
        let _guard = AbsolutePathBufGuard::new(&temp_dir);
        let config: ConfigRequirementsToml = from_str(
            r#"
            [permissions.filesystem]
            deny_read = ["./private/**/*.txt"]
        "#,
        )?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.filesystem,
            Some(Sourced::new(
                FilesystemConstraints {
                    deny_read: vec![
                        FilesystemDenyReadPattern::from_input("./private/**/*.txt")
                            .expect("normalize glob pattern"),
                    ],
                },
                RequirementSource::Unknown,
            ))
        );
        Ok(())
    }

    #[test]
    fn deserialize_apps_requirements() -> Result<()> {
        let toml_str = r#"
            [apps.connector_123123]
            enabled = false
        "#;
        let requirements: ConfigRequirementsToml = from_str(toml_str)?;

        assert_eq!(
            requirements.apps,
            Some(AppsRequirementsToml {
                apps: BTreeMap::from([(
                    "connector_123123".to_string(),
                    AppRequirementToml {
                        enabled: Some(false),
                        tools: None,
                    },
                )]),
            })
        );
        Ok(())
    }

    #[test]
    fn deserialize_apps_tool_requirements() -> Result<()> {
        let toml_str = r#"
            [apps.connector_123123.tools."calendar/list_events"]
            approval_mode = "approve"
        "#;
        let requirements: ConfigRequirementsToml = from_str(toml_str)?;

        assert_eq!(
            requirements.apps,
            Some(AppsRequirementsToml {
                apps: BTreeMap::from([(
                    "connector_123123".to_string(),
                    AppRequirementToml {
                        enabled: None,
                        tools: Some(AppToolsRequirementsToml {
                            tools: BTreeMap::from([(
                                "calendar/list_events".to_string(),
                                AppToolRequirementToml {
                                    approval_mode: Some(AppToolApproval::Approve),
                                },
                            )]),
                        }),
                    },
                )]),
            })
        );
        Ok(())
    }

    fn apps_requirements(entries: &[(&str, Option<bool>)]) -> AppsRequirementsToml {
        AppsRequirementsToml {
            apps: entries
                .iter()
                .map(|(app_id, enabled)| {
                    (
                        (*app_id).to_string(),
                        AppRequirementToml {
                            enabled: *enabled,
                            tools: None,
                        },
                    )
                })
                .collect(),
        }
    }

    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 merge_app_requirements_descending_unions_distinct_apps() {
        let mut merged = apps_requirements(&[("connector_high", Some(false))]);
        let lower = apps_requirements(&[("connector_low", Some(true))]);

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            apps_requirements(&[
                ("connector_high", Some(false)),
                ("connector_low", Some(true))
            ]),
        );
    }

    #[test]
    fn merge_app_requirements_descending_prefers_false_from_lower_precedence() {
        let mut merged = apps_requirements(&[("connector_123123", Some(true))]);
        let lower = apps_requirements(&[("connector_123123", Some(false))]);

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            apps_requirements(&[("connector_123123", Some(false))]),
        );
    }

    #[test]
    fn merge_app_requirements_descending_keeps_higher_true_when_lower_is_unset() {
        let mut merged = apps_requirements(&[("connector_123123", Some(true))]);
        let lower = apps_requirements(&[("connector_123123", None)]);

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            apps_requirements(&[("connector_123123", Some(true))]),
        );
    }

    #[test]
    fn merge_app_requirements_descending_uses_lower_value_when_higher_missing() {
        let mut merged = apps_requirements(&[]);
        let lower = apps_requirements(&[("connector_123123", Some(true))]);

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            apps_requirements(&[("connector_123123", Some(true))]),
        );
    }

    #[test]
    fn merge_app_requirements_descending_preserves_higher_false_when_lower_missing_app() {
        let mut merged = apps_requirements(&[("connector_123123", Some(false))]);
        let lower = apps_requirements(&[]);

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            apps_requirements(&[("connector_123123", Some(false))]),
        );
    }

    #[test]
    fn merge_app_requirements_descending_preserves_higher_tool_approval_mode() {
        let mut merged = app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Approve,
        );
        let lower = app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Prompt,
        );

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            app_tool_requirements(
                "connector_123123",
                "calendar/list_events",
                AppToolApproval::Approve,
            )
        );
    }

    #[test]
    fn merge_app_requirements_descending_uses_lower_tool_approval_when_higher_missing() {
        let mut merged = apps_requirements(&[("connector_123123", None)]);
        let lower = app_tool_requirements(
            "connector_123123",
            "calendar/list_events",
            AppToolApproval::Approve,
        );

        merge_app_requirements_descending(&mut merged, lower);

        assert_eq!(
            merged,
            app_tool_requirements(
                "connector_123123",
                "calendar/list_events",
                AppToolApproval::Approve,
            )
        );
    }

    #[test]
    fn merge_unset_fields_merges_apps_across_sources_with_enabled_evaluation() {
        let higher_source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let lower_source = RequirementSource::MdmManagedPreferences {
            domain: "com.openai.codex".to_string(),
            key: "requirements_toml_base64".to_string(),
        };
        let mut target = ConfigRequirementsWithSources::default();

        target.merge_unset_fields(
            higher_source.clone(),
            ConfigRequirementsToml {
                apps: Some(apps_requirements(&[
                    ("connector_high", Some(true)),
                    ("connector_shared", Some(true)),
                ])),
                ..Default::default()
            },
        );
        target.merge_unset_fields(
            lower_source,
            ConfigRequirementsToml {
                apps: Some(apps_requirements(&[
                    ("connector_low", Some(false)),
                    ("connector_shared", Some(false)),
                ])),
                ..Default::default()
            },
        );

        let apps = target.apps.expect("apps should be present");
        assert_eq!(
            apps.value,
            apps_requirements(&[
                ("connector_high", Some(true)),
                ("connector_low", Some(false)),
                ("connector_shared", Some(false)),
            ])
        );
        assert_eq!(apps.source, higher_source);
    }

    #[test]
    fn merge_unset_fields_apps_empty_higher_source_does_not_block_lower_disables() {
        let mut target = ConfigRequirementsWithSources::default();

        target.merge_unset_fields(
            RequirementSource::LegacyManagedConfigTomlFromMdm,
            ConfigRequirementsToml {
                apps: Some(apps_requirements(&[])),
                ..Default::default()
            },
        );
        target.merge_unset_fields(
            RequirementSource::LegacyManagedConfigTomlFromMdm,
            ConfigRequirementsToml {
                apps: Some(apps_requirements(&[("connector_123123", Some(false))])),
                ..Default::default()
            },
        );

        assert_eq!(
            target.apps.map(|apps| apps.value),
            Some(apps_requirements(&[("connector_123123", Some(false))])),
        );
    }

    #[test]
    fn constraint_error_includes_requirement_source() -> Result<()> {
        let source: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["on-request"]
                allowed_approvals_reviewers = ["auto_review"]
                allowed_sandbox_modes = ["read-only"]
            "#,
        )?;

        let requirements_toml_file = system_requirements_toml_file_for_test()?;
        let source_location = RequirementSource::SystemRequirementsToml {
            file: requirements_toml_file,
        };

        let mut target = ConfigRequirementsWithSources::default();
        target.merge_unset_fields(source_location.clone(), source);
        let requirements = ConfigRequirements::try_from(target)?;

        assert_eq!(
            requirements.approval_policy.can_set(&AskForApproval::Never),
            Err(ConstraintError::InvalidValue {
                field_name: "approval_policy",
                candidate: "Never".into(),
                allowed: "[OnRequest]".into(),
                requirement_source: source_location.clone(),
            })
        );
        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::Disabled),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "DangerFullAccess".into(),
                allowed: "[ReadOnly]".into(),
                requirement_source: source_location.clone(),
            })
        );
        assert_eq!(
            requirements
                .approvals_reviewer
                .can_set(&ApprovalsReviewer::User),
            Err(ConstraintError::InvalidValue {
                field_name: "approvals_reviewer",
                candidate: "User".into(),
                allowed: "[AutoReview]".into(),
                requirement_source: source_location,
            })
        );

        Ok(())
    }

    #[test]
    fn constraint_error_includes_composite_requirement_source() -> Result<()> {
        let source: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["on-request"]
            "#,
        )?;

        let source_location = RequirementSource::composite([
            RequirementSource::MdmManagedPreferences {
                domain: "com.openai.codex".to_string(),
                key: "requirements_toml_base64".to_string(),
            },
            RequirementSource::LegacyManagedConfigTomlFromMdm,
        ]);

        let mut target = ConfigRequirementsWithSources::default();
        target.merge_unset_fields(source_location.clone(), source);
        let requirements = ConfigRequirements::try_from(target)?;

        assert_eq!(
            requirements.approval_policy.can_set(&AskForApproval::Never),
            Err(ConstraintError::InvalidValue {
                field_name: "approval_policy",
                candidate: "Never".into(),
                allowed: "[OnRequest]".into(),
                requirement_source: source_location,
            })
        );

        Ok(())
    }

    #[test]
    fn constrained_fields_store_requirement_source() -> Result<()> {
        let source: ConfigRequirementsToml = from_str(
            r#"
                allowed_approval_policies = ["on-request"]
                allowed_approvals_reviewers = ["auto_review"]
                allowed_sandbox_modes = ["read-only"]
                allowed_web_search_modes = ["cached"]
                enforce_residency = "us"
                [features]
                personality = true
            "#,
        )?;

        let source_location = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut target = ConfigRequirementsWithSources::default();
        target.merge_unset_fields(source_location.clone(), source);
        let requirements = ConfigRequirements::try_from(target)?;

        assert_eq!(
            requirements.approval_policy.source,
            Some(source_location.clone())
        );
        assert_eq!(
            requirements.approvals_reviewer.source,
            Some(source_location.clone())
        );
        assert_eq!(
            requirements.permission_profile.source,
            Some(source_location.clone())
        );
        assert_eq!(
            requirements.web_search_mode.source,
            Some(source_location.clone())
        );
        assert_eq!(
            requirements
                .feature_requirements
                .as_ref()
                .map(|requirements| requirements.source.clone()),
            Some(source_location.clone())
        );
        assert_eq!(requirements.enforce_residency.source, Some(source_location));

        Ok(())
    }

    #[test]
    fn deserialize_allowed_approval_policies() -> Result<()> {
        let toml_str = r#"
            allowed_approval_policies = ["untrusted", "on-request"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.approval_policy.value(),
            AskForApproval::UnlessTrusted,
            "currently, there is no way to specify the default value for approval policy in the toml, so it picks the first allowed value"
        );
        assert!(
            requirements
                .approval_policy
                .can_set(&AskForApproval::UnlessTrusted)
                .is_ok()
        );
        assert_eq!(
            requirements
                .approval_policy
                .can_set(&AskForApproval::OnFailure),
            Err(ConstraintError::InvalidValue {
                field_name: "approval_policy",
                candidate: "OnFailure".into(),
                allowed: "[UnlessTrusted, OnRequest]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        assert!(
            requirements
                .approval_policy
                .can_set(&AskForApproval::OnRequest)
                .is_ok()
        );
        assert_eq!(
            requirements.approval_policy.can_set(&AskForApproval::Never),
            Err(ConstraintError::InvalidValue {
                field_name: "approval_policy",
                candidate: "Never".into(),
                allowed: "[UnlessTrusted, OnRequest]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        assert!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::read_only())
                .is_ok()
        );

        Ok(())
    }

    #[test]
    fn deserialize_allowed_approvals_reviewers() -> Result<()> {
        let toml_str = r#"
            allowed_approvals_reviewers = ["auto_review", "user"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.approvals_reviewer.value(),
            ApprovalsReviewer::AutoReview,
            "currently, there is no way to specify the default value for approvals reviewer in the toml, so it picks the first allowed value"
        );
        assert!(
            requirements
                .approvals_reviewer
                .can_set(&ApprovalsReviewer::AutoReview)
                .is_ok()
        );
        assert!(
            requirements
                .approvals_reviewer
                .can_set(&ApprovalsReviewer::User)
                .is_ok()
        );

        Ok(())
    }

    #[test]
    fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> {
        let toml_str = r#"
            [windows]
            allowed_sandbox_implementations = ["elevated"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.windows_sandbox_mode.value(),
            Some(WindowsSandboxModeToml::Elevated)
        );
        assert!(
            requirements
                .windows_sandbox_mode
                .can_set(&Some(WindowsSandboxModeToml::Elevated))
                .is_ok()
        );
        assert!(
            requirements
                .windows_sandbox_mode
                .can_set(&Some(WindowsSandboxModeToml::Unelevated))
                .is_err()
        );
        assert!(requirements.windows_sandbox_mode.can_set(&None).is_err());

        Ok(())
    }

    #[test]
    fn empty_allowed_windows_sandbox_implementations_is_rejected() -> Result<()> {
        let toml_str = r#"
            [windows]
            allowed_sandbox_implementations = []
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;

        assert_eq!(
            ConfigRequirements::try_from(with_unknown_source(config)),
            Err(ConstraintError::EmptyField {
                field_name: "windows.allowed_sandbox_implementations".to_string(),
            })
        );

        Ok(())
    }

    #[test]
    fn allowed_windows_sandbox_implementations_prefer_elevated_fallback() -> Result<()> {
        let toml_str = r#"
            [windows]
            allowed_sandbox_implementations = ["unelevated", "elevated"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.windows_sandbox_mode.value(),
            Some(WindowsSandboxModeToml::Elevated)
        );

        Ok(())
    }

    #[test]
    fn deserialize_legacy_allowed_approvals_reviewer() -> Result<()> {
        let toml_str = r#"
            allowed_approvals_reviewers = ["guardian_subagent", "user"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.approvals_reviewer.value(),
            ApprovalsReviewer::AutoReview
        );

        Ok(())
    }

    #[test]
    fn empty_allowed_approvals_reviewers_is_rejected() -> Result<()> {
        let toml_str = r#"
            allowed_approvals_reviewers = []
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let err = ConfigRequirements::try_from(with_unknown_source(config))
            .expect_err("empty approvals reviewer allow-list should be rejected");

        assert_eq!(
            err,
            ConstraintError::EmptyField {
                field_name: "allowed_approvals_reviewers".to_string(),
            }
        );

        Ok(())
    }

    #[test]
    fn deserialize_allowed_sandbox_modes() -> Result<()> {
        let toml_str = r#"
            allowed_sandbox_modes = ["read-only", "workspace-write"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
        assert!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::read_only())
                .is_ok()
        );
        let workspace_write_profile = PermissionProfile::workspace_write_with(
            &[AbsolutePathBuf::from_absolute_path(root)?],
            NetworkSandboxPolicy::Restricted,
            /*exclude_tmpdir_env_var*/ false,
            /*exclude_slash_tmp*/ false,
        );
        assert!(
            requirements
                .permission_profile
                .can_set(&workspace_write_profile)
                .is_ok()
        );
        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::Disabled),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "DangerFullAccess".into(),
                allowed: "[ReadOnly, WorkspaceWrite]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::External {
                    network: NetworkSandboxPolicy::Restricted,
                }),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "ExternalSandbox".into(),
                allowed: "[ReadOnly, WorkspaceWrite]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );

        Ok(())
    }

    #[test]
    fn deserialize_remote_sandbox_config_requires_hostname_patterns_list() -> Result<()> {
        let toml_str = r#"
            [[remote_sandbox_config]]
            hostname_patterns = ["*.org", "runner-??.ci"]
            allowed_sandbox_modes = ["read-only", "workspace-write"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;

        assert_eq!(
            config.remote_sandbox_config,
            Some(vec![RemoteSandboxConfigToml {
                hostname_patterns: vec!["*.org".to_string(), "runner-??.ci".to_string()],
                allowed_sandbox_modes: vec![
                    SandboxModeRequirement::ReadOnly,
                    SandboxModeRequirement::WorkspaceWrite,
                ],
            }])
        );

        let err = from_str::<ConfigRequirementsToml>(
            r#"
                [[remote_sandbox_config]]
                hostname_patterns = "*.org"
                allowed_sandbox_modes = ["read-only"]
            "#,
        )
        .expect_err("hostname_patterns should be list-only");
        assert!(
            err.to_string().contains("invalid type: string"),
            "unexpected error: {err}"
        );

        Ok(())
    }

    #[test]
    fn remote_sandbox_config_first_match_overrides_top_level() -> Result<()> {
        let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut requirements_toml: ConfigRequirementsToml = from_str(
            r#"
                allowed_sandbox_modes = ["read-only"]

                [[remote_sandbox_config]]
                hostname_patterns = ["build-*.example.com"]
                allowed_sandbox_modes = ["read-only", "workspace-write"]

                [[remote_sandbox_config]]
                hostname_patterns = ["build-01.example.com"]
                allowed_sandbox_modes = ["read-only", "danger-full-access"]
            "#,
        )?;
        requirements_toml.apply_remote_sandbox_config(Some("BUILD-01.EXAMPLE.COM."));
        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(source.clone(), requirements_toml);

        assert_eq!(
            requirements_with_sources
                .allowed_sandbox_modes
                .as_ref()
                .map(|sourced| sourced.value.clone()),
            Some(vec![
                SandboxModeRequirement::ReadOnly,
                SandboxModeRequirement::WorkspaceWrite,
            ])
        );

        let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
        let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
        let workspace_write_profile = PermissionProfile::workspace_write_with(
            &[AbsolutePathBuf::from_absolute_path(root)?],
            NetworkSandboxPolicy::Restricted,
            /*exclude_tmpdir_env_var*/ false,
            /*exclude_slash_tmp*/ false,
        );
        assert!(
            requirements
                .permission_profile
                .can_set(&workspace_write_profile)
                .is_ok()
        );
        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::Disabled),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "DangerFullAccess".into(),
                allowed: "[ReadOnly, WorkspaceWrite]".into(),
                requirement_source: source,
            })
        );

        Ok(())
    }

    #[test]
    fn remote_sandbox_config_non_match_preserves_top_level() -> Result<()> {
        let mut requirements_toml: ConfigRequirementsToml = from_str(
            r#"
                allowed_sandbox_modes = ["read-only"]

                [[remote_sandbox_config]]
                hostname_patterns = ["build-*.example.com"]
                allowed_sandbox_modes = ["read-only", "workspace-write"]
            "#,
        )?;
        requirements_toml.apply_remote_sandbox_config(Some("laptop.example.com"));
        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, requirements_toml);
        let requirements = ConfigRequirements::try_from(requirements_with_sources)?;

        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::Disabled),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "DangerFullAccess".into(),
                allowed: "[ReadOnly]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );

        Ok(())
    }

    #[test]
    fn remote_sandbox_config_does_not_override_higher_precedence_sandbox_modes() -> Result<()> {
        let high_source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut high_precedence: ConfigRequirementsToml = from_str(
            r#"
                allowed_sandbox_modes = ["read-only"]
            "#,
        )?;
        high_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com"));

        let mut low_precedence: ConfigRequirementsToml = from_str(
            r#"
                [[remote_sandbox_config]]
                hostname_patterns = ["runner-*.ci.example.com"]
                allowed_sandbox_modes = ["read-only", "workspace-write"]
            "#,
        )?;
        low_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com"));

        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(high_source.clone(), high_precedence);
        requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, low_precedence);
        let requirements = ConfigRequirements::try_from(requirements_with_sources)?;

        assert_eq!(
            requirements
                .permission_profile
                .can_set(&PermissionProfile::workspace_write()),
            Err(ConstraintError::InvalidValue {
                field_name: "sandbox_mode",
                candidate: "WorkspaceWrite".into(),
                allowed: "[ReadOnly]".into(),
                requirement_source: high_source,
            })
        );

        Ok(())
    }

    #[test]
    fn deserialize_allowed_web_search_modes() -> Result<()> {
        let toml_str = r#"
            allowed_web_search_modes = ["cached"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(requirements.web_search_mode.value(), WebSearchMode::Cached);
        assert!(
            requirements
                .web_search_mode
                .can_set(&WebSearchMode::Disabled)
                .is_ok()
        );
        assert_eq!(
            requirements.web_search_mode.can_set(&WebSearchMode::Live),
            Err(ConstraintError::InvalidValue {
                field_name: "web_search_mode",
                candidate: "Live".into(),
                allowed: "[Disabled, Cached]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        assert!(
            requirements
                .web_search_mode
                .can_set(&WebSearchMode::Cached)
                .is_ok()
        );

        Ok(())
    }

    #[test]
    fn allowed_web_search_modes_allows_disabled() -> Result<()> {
        let toml_str = r#"
            allowed_web_search_modes = ["disabled"]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.web_search_mode.value(),
            WebSearchMode::Disabled
        );
        assert!(
            requirements
                .web_search_mode
                .can_set(&WebSearchMode::Disabled)
                .is_ok()
        );
        assert_eq!(
            requirements.web_search_mode.can_set(&WebSearchMode::Cached),
            Err(ConstraintError::InvalidValue {
                field_name: "web_search_mode",
                candidate: "Cached".into(),
                allowed: "[Disabled]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        Ok(())
    }

    #[test]
    fn allowed_web_search_modes_empty_restricts_to_disabled() -> Result<()> {
        let toml_str = r#"
            allowed_web_search_modes = []
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.web_search_mode.value(),
            WebSearchMode::Disabled
        );
        assert!(
            requirements
                .web_search_mode
                .can_set(&WebSearchMode::Disabled)
                .is_ok()
        );
        assert_eq!(
            requirements.web_search_mode.can_set(&WebSearchMode::Cached),
            Err(ConstraintError::InvalidValue {
                field_name: "web_search_mode",
                candidate: "Cached".into(),
                allowed: "[Disabled]".into(),
                requirement_source: RequirementSource::Unknown,
            })
        );
        Ok(())
    }

    #[test]
    fn deserialize_feature_requirements() -> Result<()> {
        let toml_str = r#"
            [features]
            apps = false
            personality = true
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;

        assert_eq!(
            requirements.feature_requirements,
            Some(Sourced::new(
                FeatureRequirementsToml {
                    entries: BTreeMap::from([
                        ("apps".to_string(), false),
                        ("personality".to_string(), true),
                    ]),
                },
                RequirementSource::Unknown,
            ))
        );

        Ok(())
    }

    #[test]
    fn deserialize_managed_hooks_requirements() -> Result<()> {
        let toml_str = r#"
managed_dir = "/enterprise/hooks"
windows_managed_dir = 'C:\enterprise\hooks'

[[PreToolUse]]
matcher = "^Bash$"

[[PreToolUse.hooks]]
type = "command"
command = "python3 /enterprise/hooks/pre.py"
timeout = 10
statusMessage = "checking"
        "#;
        let hooks: ManagedHooksRequirementsToml = from_str(toml_str)?;

        assert_eq!(
            hooks.managed_dir.as_deref(),
            Some(std::path::Path::new("/enterprise/hooks"))
        );
        assert_eq!(hooks.handler_count(), 1);
        assert_eq!(hooks.hooks.pre_tool_use.len(), 1);
        Ok(())
    }

    #[test]
    fn merge_unset_fields_does_not_overwrite_existing_hooks() -> Result<()> {
        let mut target = ConfigRequirementsWithSources::default();
        target.merge_unset_fields(
            RequirementSource::LegacyManagedConfigTomlFromMdm,
            from_str::<ConfigRequirementsToml>(
                r#"
[hooks]
managed_dir = "/cloud/hooks"

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 /cloud/hooks/pre.py"
                "#,
            )?,
        );
        target.merge_unset_fields(
            RequirementSource::SystemRequirementsToml {
                file: system_requirements_toml_file_for_test()?,
            },
            from_str::<ConfigRequirementsToml>(
                r#"
[hooks]
managed_dir = "/system/hooks"

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 /system/hooks/pre.py"
                "#,
            )?,
        );

        assert_eq!(
            target
                .hooks
                .as_ref()
                .and_then(|hooks| hooks.value.managed_dir.as_ref())
                .map(std::path::PathBuf::as_path),
            Some(std::path::Path::new("/cloud/hooks"))
        );
        assert_eq!(
            target.hooks.as_ref().map(|hooks| hooks.source.clone()),
            Some(RequirementSource::LegacyManagedConfigTomlFromMdm)
        );
        Ok(())
    }

    #[test]
    fn managed_hooks_constraint_rejects_drift() -> Result<()> {
        let config: ConfigRequirementsToml = from_str(
            r#"
[hooks]
managed_dir = "/enterprise/hooks"

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 /enterprise/hooks/pre.py"
            "#,
        )?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
        let mut managed_hooks = requirements
            .managed_hooks
            .expect("expected managed hooks requirements");

        let err = managed_hooks
            .set(ManagedHooksRequirementsToml {
                managed_dir: Some(std::path::PathBuf::from("/other/hooks")),
                windows_managed_dir: None,
                hooks: HookEventsToml::default(),
            })
            .expect_err("managed hooks should reject drift");

        assert!(matches!(
            err,
            ConstraintError::InvalidValue {
                field_name: "hooks",
                requirement_source: RequirementSource::Unknown,
                ..
            }
        ));
        Ok(())
    }

    #[test]
    fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
        let toml_str = r#"
            [experimental_network]
            enabled = true
            allow_upstream_proxy = false
            dangerously_allow_all_unix_sockets = true
            managed_allowed_domains_only = true
            allow_local_binding = false

            [experimental_network.domains]
            "api.example.com" = "allow"
            "*.openai.com" = "allow"
            "blocked.example.com" = "deny"

            [experimental_network.unix_sockets]
            "/tmp/example.sock" = "allow"
            "/tmp/blocked.sock" = "deny"
        "#;

        let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?);

        let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
        let sourced_network = requirements
            .network
            .expect("network requirements should be preserved as constraints");

        assert_eq!(sourced_network.source, source);
        assert_eq!(sourced_network.value.enabled, Some(true));
        assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
        assert_eq!(
            sourced_network.value.dangerously_allow_all_unix_sockets,
            Some(true)
        );
        assert_eq!(
            sourced_network.value.domains.as_ref(),
            Some(&NetworkDomainPermissionsToml {
                entries: BTreeMap::from([
                    (
                        "*.openai.com".to_string(),
                        NetworkDomainPermissionToml::Allow,
                    ),
                    (
                        "api.example.com".to_string(),
                        NetworkDomainPermissionToml::Allow,
                    ),
                    (
                        "blocked.example.com".to_string(),
                        NetworkDomainPermissionToml::Deny,
                    ),
                ]),
            })
        );
        assert_eq!(
            sourced_network.value.managed_allowed_domains_only,
            Some(true)
        );
        assert_eq!(
            sourced_network.value.unix_sockets.as_ref(),
            Some(&NetworkUnixSocketPermissionsToml {
                entries: BTreeMap::from([
                    (
                        "/tmp/blocked.sock".to_string(),
                        NetworkUnixSocketPermissionToml::Deny,
                    ),
                    (
                        "/tmp/example.sock".to_string(),
                        NetworkUnixSocketPermissionToml::Allow,
                    ),
                ]),
            })
        );
        assert_eq!(sourced_network.value.allow_local_binding, Some(false));

        Ok(())
    }

    #[test]
    fn legacy_network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
        let toml_str = r#"
            [experimental_network]
            enabled = true
            allow_upstream_proxy = false
            dangerously_allow_all_unix_sockets = true
            allowed_domains = ["api.example.com", "*.openai.com"]
            managed_allowed_domains_only = true
            denied_domains = ["blocked.example.com"]
            allow_unix_sockets = ["/tmp/example.sock"]
            allow_local_binding = false
        "#;

        let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?);

        let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
        let sourced_network = requirements
            .network
            .expect("network requirements should be preserved as constraints");

        assert_eq!(sourced_network.source, source);
        assert_eq!(sourced_network.value.enabled, Some(true));
        assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false));
        assert_eq!(
            sourced_network.value.dangerously_allow_all_unix_sockets,
            Some(true)
        );
        assert_eq!(
            sourced_network.value.domains.as_ref(),
            Some(&NetworkDomainPermissionsToml {
                entries: BTreeMap::from([
                    (
                        "*.openai.com".to_string(),
                        NetworkDomainPermissionToml::Allow,
                    ),
                    (
                        "api.example.com".to_string(),
                        NetworkDomainPermissionToml::Allow,
                    ),
                    (
                        "blocked.example.com".to_string(),
                        NetworkDomainPermissionToml::Deny,
                    ),
                ]),
            })
        );
        assert_eq!(
            sourced_network.value.managed_allowed_domains_only,
            Some(true)
        );
        assert_eq!(
            sourced_network.value.unix_sockets.as_ref(),
            Some(&NetworkUnixSocketPermissionsToml {
                entries: BTreeMap::from([(
                    "/tmp/example.sock".to_string(),
                    NetworkUnixSocketPermissionToml::Allow,
                )]),
            })
        );
        assert_eq!(sourced_network.value.allow_local_binding, Some(false));

        Ok(())
    }

    #[test]
    fn mixed_legacy_and_canonical_network_requirements_are_rejected() {
        let err = from_str::<ConfigRequirementsToml>(
            r#"
                [experimental_network]
                allowed_domains = ["api.example.com"]

                [experimental_network.domains]
                "*.openai.com" = "allow"
            "#,
        )
        .expect_err("mixed network domain shapes should fail");

        assert!(
            err.to_string()
                .contains("`experimental_network.domains` cannot be combined"),
            "unexpected error: {err:#}"
        );

        let err = from_str::<ConfigRequirementsToml>(
            r#"
                [experimental_network]
                allow_unix_sockets = ["/tmp/example.sock"]

                [experimental_network.unix_sockets]
                "/tmp/another.sock" = "allow"
            "#,
        )
        .expect_err("mixed network unix socket shapes should fail");

        assert!(
            err.to_string()
                .contains("`experimental_network.unix_sockets` cannot be combined"),
            "unexpected error: {err:#}"
        );
    }

    #[test]
    fn network_permission_containers_project_allowed_and_denied_entries() {
        let domains = NetworkDomainPermissionsToml {
            entries: BTreeMap::from([
                (
                    "*.openai.com".to_string(),
                    NetworkDomainPermissionToml::Allow,
                ),
                (
                    "api.example.com".to_string(),
                    NetworkDomainPermissionToml::Allow,
                ),
                (
                    "blocked.example.com".to_string(),
                    NetworkDomainPermissionToml::Deny,
                ),
            ]),
        };
        let unix_sockets = NetworkUnixSocketPermissionsToml {
            entries: BTreeMap::from([
                (
                    "/tmp/example.sock".to_string(),
                    NetworkUnixSocketPermissionToml::Allow,
                ),
                (
                    "/tmp/ignored.sock".to_string(),
                    NetworkUnixSocketPermissionToml::Deny,
                ),
            ]),
        };

        assert_eq!(
            domains.allowed_domains(),
            Some(vec![
                "*.openai.com".to_string(),
                "api.example.com".to_string()
            ])
        );
        assert_eq!(
            domains.denied_domains(),
            Some(vec!["blocked.example.com".to_string()])
        );
        assert_eq!(
            NetworkDomainPermissionsToml {
                entries: BTreeMap::from([(
                    "api.example.com".to_string(),
                    NetworkDomainPermissionToml::Allow,
                )]),
            }
            .denied_domains(),
            None
        );
        assert_eq!(
            unix_sockets.allow_unix_sockets(),
            vec!["/tmp/example.sock".to_string()]
        );
    }

    #[test]
    fn deserialize_mcp_server_requirements() -> Result<()> {
        let toml_str = r#"
            [mcp_servers.docs.identity]
            command = "codex-mcp"

            [mcp_servers.remote.identity]
            url = "https://example.com/mcp"
        "#;
        let requirements: ConfigRequirements =
            with_unknown_source(from_str(toml_str)?).try_into()?;

        assert_eq!(
            requirements.mcp_servers,
            Some(Sourced::new(
                BTreeMap::from([
                    (
                        "docs".to_string(),
                        McpServerRequirement {
                            identity: McpServerIdentity::Command {
                                command: "codex-mcp".to_string(),
                            },
                        },
                    ),
                    (
                        "remote".to_string(),
                        McpServerRequirement {
                            identity: McpServerIdentity::Url {
                                url: "https://example.com/mcp".to_string(),
                            },
                        },
                    ),
                ]),
                RequirementSource::Unknown,
            ))
        );
        Ok(())
    }

    #[test]
    fn deserialize_plugin_mcp_server_requirements() -> Result<()> {
        let toml_str = r#"
            [plugins."sample@test".mcp_servers.sample.identity]
            command = "sample-mcp"

            [plugins."remote@test".mcp_servers.remote.identity]
            url = "https://example.com/mcp"
        "#;
        let requirements: ConfigRequirements =
            with_unknown_source(from_str(toml_str)?).try_into()?;

        assert_eq!(
            requirements.plugins,
            Some(Sourced::new(
                BTreeMap::from([
                    (
                        "remote@test".to_string(),
                        PluginRequirementsToml {
                            mcp_servers: Some(BTreeMap::from([(
                                "remote".to_string(),
                                McpServerRequirement {
                                    identity: McpServerIdentity::Url {
                                        url: "https://example.com/mcp".to_string(),
                                    },
                                },
                            )])),
                        },
                    ),
                    (
                        "sample@test".to_string(),
                        PluginRequirementsToml {
                            mcp_servers: Some(BTreeMap::from([(
                                "sample".to_string(),
                                McpServerRequirement {
                                    identity: McpServerIdentity::Command {
                                        command: "sample-mcp".to_string(),
                                    },
                                },
                            )])),
                        },
                    ),
                ]),
                RequirementSource::Unknown,
            ))
        );
        Ok(())
    }

    #[test]
    fn deserialize_exec_policy_requirements() -> Result<()> {
        let toml_str = r#"
            [rules]
            prefix_rules = [
                { pattern = [{ token = "rm" }], decision = "forbidden" },
            ]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
        let policy = requirements.exec_policy.expect("exec policy").value;

        assert_eq!(
            policy.as_ref().check(&tokens(&["rm", "-rf"]), &|_| {
                panic!("rule should match so heuristic should not be called");
            }),
            Evaluation {
                decision: Decision::Forbidden,
                matched_rules: vec![RuleMatch::PrefixRuleMatch {
                    matched_prefix: tokens(&["rm"]),
                    decision: Decision::Forbidden,
                    resolved_program: None,
                    justification: None,
                }],
            }
        );

        Ok(())
    }

    #[test]
    fn exec_policy_error_includes_requirement_source() -> Result<()> {
        let toml_str = r#"
            [rules]
            prefix_rules = [
                { pattern = [{ token = "rm" }] },
            ]
        "#;
        let config: ConfigRequirementsToml = from_str(toml_str)?;
        let requirements_toml_file = system_requirements_toml_file_for_test()?;
        let source_location = RequirementSource::SystemRequirementsToml {
            file: requirements_toml_file,
        };

        let mut requirements_with_sources = ConfigRequirementsWithSources::default();
        requirements_with_sources.merge_unset_fields(source_location.clone(), config);
        let err = ConfigRequirements::try_from(requirements_with_sources)
            .expect_err("invalid exec policy");

        assert_eq!(
            err,
            ConstraintError::ExecPolicyParse {
                requirement_source: source_location,
                reason: "rules prefix_rule at index 0 is missing a decision".to_string(),
            }
        );

        Ok(())
    }
}
