use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;

use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use codex_config::HookEventsToml;
use codex_config::HookHandlerConfig;
use codex_config::HookStateToml;
use codex_config::HooksFile;
use codex_config::ManagedHooksRequirementsToml;
use codex_config::MatcherGroup;
use codex_config::RequirementSource;
use codex_config::TomlValue;
use codex_config::version_for_toml;
use codex_plugin::PluginHookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;

use super::ConfiguredHandler;
use super::HookListEntry;
use crate::config_rules::hook_states_from_stack;
use crate::events::common::matcher_pattern_for_event;
use crate::events::common::validate_matcher_pattern;
use codex_protocol::protocol::HookHandlerType;
use codex_protocol::protocol::HookSource;
use codex_protocol::protocol::HookTrustStatus;

pub(crate) struct DiscoveryResult {
    pub handlers: Vec<ConfiguredHandler>,
    pub hook_entries: Vec<HookListEntry>,
    pub warnings: Vec<String>,
}

struct HookHandlerSource<'a> {
    path: &'a AbsolutePathBuf,
    key_source: String,
    source: HookSource,
    is_managed: bool,
    bypass_hook_trust: bool,
    hook_states: &'a HashMap<String, HookStateToml>,
    env: HashMap<String, String>,
    plugin_id: Option<String>,
}

#[derive(Clone, Copy)]
struct HookDiscoveryPolicy {
    allow_managed_hooks_only: bool,
    bypass_hook_trust: bool,
}

impl HookDiscoveryPolicy {
    fn allows(self, source: &HookHandlerSource<'_>) -> bool {
        !self.allow_managed_hooks_only || source.is_managed
    }
}

pub(crate) fn discover_handlers(
    config_layer_stack: Option<&ConfigLayerStack>,
    plugin_hook_sources: Vec<PluginHookSource>,
    plugin_hook_load_warnings: Vec<String>,
    bypass_hook_trust: bool,
) -> DiscoveryResult {
    let mut handlers = Vec::new();
    let mut hook_entries = Vec::new();
    let mut warnings = plugin_hook_load_warnings;
    let mut display_order = 0_i64;
    let mut visited_json_hook_folders = HashSet::new();
    let hook_states = hook_states_from_stack(config_layer_stack);
    let policy = HookDiscoveryPolicy {
        allow_managed_hooks_only: config_layer_stack.is_some_and(|config_layer_stack| {
            config_layer_stack
                .requirements()
                .allow_managed_hooks_only
                .as_ref()
                .is_some_and(|requirement| requirement.value)
        }),
        bypass_hook_trust,
    };

    if let Some(config_layer_stack) = config_layer_stack {
        append_managed_requirement_handlers(
            &mut handlers,
            &mut hook_entries,
            &mut warnings,
            &mut display_order,
            config_layer_stack,
            &hook_states,
            policy,
        );

        for layer in config_layer_stack.get_layers(
            ConfigLayerStackOrdering::LowestPrecedenceFirst,
            /*include_disabled*/ false,
        ) {
            let (hook_source, is_managed) = hook_metadata_for_config_layer_source(&layer.name);
            let policy_path = config_toml_source_path(layer);
            let policy_source = HookHandlerSource {
                path: &policy_path,
                key_source: policy_path.display().to_string(),
                source: hook_source,
                is_managed,
                bypass_hook_trust: false,
                hook_states: &hook_states,
                env: HashMap::new(),
                plugin_id: None,
            };
            if !policy.allows(&policy_source) {
                continue;
            }
            let json_hooks = match layer.hooks_config_folder() {
                Some(config_folder) if visited_json_hook_folders.insert(config_folder.clone()) => {
                    load_hooks_json(Some(config_folder.as_path()), &mut warnings)
                }
                _ => None,
            };
            let toml_hooks = load_toml_hooks_from_layer(layer, &mut warnings);

            if let (Some((json_source_path, json_events)), Some((toml_source_path, toml_events))) =
                (&json_hooks, &toml_hooks)
                && !json_events.is_empty()
                && !toml_events.is_empty()
            {
                warnings.push(format!(
                    "loading hooks from both {} and {}; prefer a single representation for this layer",
                    json_source_path.display(),
                    toml_source_path.display()
                ));
            }

            for (source_path, hook_events) in [json_hooks, toml_hooks].into_iter().flatten() {
                append_hook_events(
                    &mut handlers,
                    &mut hook_entries,
                    &mut warnings,
                    &mut display_order,
                    HookHandlerSource {
                        path: &source_path,
                        key_source: source_path.display().to_string(),
                        source: hook_source,
                        is_managed,
                        bypass_hook_trust: policy.bypass_hook_trust,
                        hook_states: &hook_states,
                        env: HashMap::new(),
                        plugin_id: None,
                    },
                    hook_events,
                    policy,
                );
            }
        }
    }

    append_plugin_hook_sources(
        &mut handlers,
        &mut hook_entries,
        &mut warnings,
        &mut display_order,
        plugin_hook_sources,
        &hook_states,
        policy,
    );

    DiscoveryResult {
        handlers,
        hook_entries,
        warnings,
    }
}

fn append_managed_requirement_handlers(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    config_layer_stack: &ConfigLayerStack,
    hook_states: &HashMap<String, HookStateToml>,
    policy: HookDiscoveryPolicy,
) {
    let Some(managed_hooks) = config_layer_stack.requirements().managed_hooks.as_ref() else {
        return;
    };
    let source_path = managed_hooks_source_path(managed_hooks.get(), managed_hooks.source.as_ref());
    append_hook_events(
        handlers,
        hook_entries,
        warnings,
        display_order,
        HookHandlerSource {
            path: &source_path,
            key_source: source_path.display().to_string(),
            source: hook_source_for_requirement_source(managed_hooks.source.as_ref()),
            is_managed: true,
            bypass_hook_trust: false,
            hook_states,
            env: HashMap::new(),
            plugin_id: None,
        },
        managed_hooks.get().hooks.clone(),
        policy,
    );
}

fn append_plugin_hook_sources(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    plugin_hook_sources: Vec<PluginHookSource>,
    hook_states: &HashMap<String, HookStateToml>,
    policy: HookDiscoveryPolicy,
) {
    for source in plugin_hook_sources {
        let PluginHookSource {
            plugin_root,
            plugin_id,
            plugin_data_root,
            source_path,
            source_relative_path,
            hooks,
        } = source;
        let mut env = HashMap::new();
        let plugin_root_value = plugin_root.display().to_string();
        let plugin_data_root_value = plugin_data_root.display().to_string();
        env.insert("PLUGIN_ROOT".to_string(), plugin_root_value.clone());
        // For OOTB compat with existing plugins that use this env var.
        env.insert("CLAUDE_PLUGIN_ROOT".to_string(), plugin_root_value);
        env.insert("PLUGIN_DATA".to_string(), plugin_data_root_value.clone());
        // For OOTB compat with existing plugins that use this env var.
        env.insert("CLAUDE_PLUGIN_DATA".to_string(), plugin_data_root_value);
        let plugin_id = plugin_id.as_key();
        append_hook_events(
            handlers,
            hook_entries,
            warnings,
            display_order,
            HookHandlerSource {
                path: &source_path,
                key_source: crate::declarations::plugin_hook_key_source(
                    plugin_id.as_str(),
                    source_relative_path.as_str(),
                ),
                source: HookSource::Plugin,
                is_managed: false,
                bypass_hook_trust: policy.bypass_hook_trust,
                hook_states,
                env,
                plugin_id: Some(plugin_id),
            },
            hooks,
            policy,
        );
    }
}

fn managed_hooks_source_path(
    managed_hooks: &ManagedHooksRequirementsToml,
    requirement_source: Option<&RequirementSource>,
) -> AbsolutePathBuf {
    if let Some(source_path) = managed_hooks.managed_dir_for_current_platform()
        && source_path.is_absolute()
        && let Ok(source_path) = AbsolutePathBuf::from_absolute_path(source_path)
    {
        return source_path;
    }

    fallback_managed_hooks_source_path(requirement_source)
}

fn fallback_managed_hooks_source_path(
    requirement_source: Option<&RequirementSource>,
) -> AbsolutePathBuf {
    match requirement_source {
        Some(RequirementSource::SystemRequirementsToml { file })
        | Some(RequirementSource::LegacyManagedConfigTomlFromFile { file }) => file.clone(),
        Some(RequirementSource::MdmManagedPreferences { domain, key }) => {
            synthetic_layer_path(&format!("<mdm:{domain}:{key}>/requirements.toml"))
        }
        Some(RequirementSource::Composite { .. }) => {
            synthetic_layer_path("<requirements-composition>/requirements.toml")
        }
        Some(RequirementSource::EnterpriseManaged { id, name }) => {
            let name = escape_xml_text(name);
            let id = escape_xml_text(id);
            synthetic_layer_path(&format!(
                "<enterprise-managed:{name}:{id}>/requirements.toml"
            ))
        }
        Some(RequirementSource::LegacyManagedConfigTomlFromMdm) => {
            synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
        }
        Some(RequirementSource::Unknown) | None => {
            synthetic_layer_path("<managed-requirements>/requirements.toml")
        }
    }
}

fn load_hooks_json(
    config_folder: Option<&Path>,
    warnings: &mut Vec<String>,
) -> Option<(AbsolutePathBuf, HookEventsToml)> {
    let source_path = config_folder?.join("hooks.json");
    if !source_path.as_path().is_file() {
        return None;
    }

    let contents = match fs::read_to_string(source_path.as_path()) {
        Ok(contents) => contents,
        Err(err) => {
            warnings.push(format!(
                "failed to read hooks config {}: {err}",
                source_path.display()
            ));
            return None;
        }
    };

    let parsed: HooksFile = match serde_json::from_str(&contents) {
        Ok(parsed) => parsed,
        Err(err) => {
            warnings.push(format!(
                "failed to parse hooks config {}: {err}",
                source_path.display()
            ));
            return None;
        }
    };

    let source_path = AbsolutePathBuf::from_absolute_path(&source_path)
        .inspect_err(|err| {
            warnings.push(format!(
                "failed to normalize hooks config path {}: {err}",
                source_path.display()
            ));
        })
        .ok()?;

    (!parsed.hooks.is_empty()).then_some((source_path, parsed.hooks))
}

fn load_toml_hooks_from_layer(
    layer: &ConfigLayerEntry,
    warnings: &mut Vec<String>,
) -> Option<(AbsolutePathBuf, HookEventsToml)> {
    let source_path = config_toml_source_path(layer);
    let hook_value = layer.config.get("hooks")?.clone();
    let parsed = match HookEventsToml::deserialize(hook_value) {
        Ok(parsed) => parsed,
        Err(err) => {
            warnings.push(format!(
                "failed to parse TOML hooks in {}: {err}",
                source_path.display()
            ));
            return None;
        }
    };

    (!parsed.is_empty()).then_some((source_path, parsed))
}

fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
    match &layer.name {
        ConfigLayerSource::System { file }
        | ConfigLayerSource::User { file, .. }
        | ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => file.clone(),
        ConfigLayerSource::Project { dot_codex_folder } => layer
            .hooks_config_folder()
            .unwrap_or_else(|| dot_codex_folder.clone())
            .join(CONFIG_TOML_FILE),
        ConfigLayerSource::Mdm { domain, key } => {
            synthetic_layer_path(&format!("<mdm:{domain}:{key}>/{CONFIG_TOML_FILE}"))
        }
        ConfigLayerSource::EnterpriseManaged { id, name } => synthetic_layer_path(&format!(
            "<enterprise-managed:{name}:{id}>/{CONFIG_TOML_FILE}"
        )),
        ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
            synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
        }
        ConfigLayerSource::SessionFlags => synthetic_layer_path("<session-flags>/config.toml"),
    }
}

fn synthetic_layer_path(path: &str) -> AbsolutePathBuf {
    #[cfg(windows)]
    {
        AbsolutePathBuf::resolve_path_against_base(path, r"C:\")
    }

    #[cfg(not(windows))]
    {
        AbsolutePathBuf::resolve_path_against_base(path, "/")
    }
}

fn escape_xml_text(value: &str) -> String {
    let mut escaped = String::with_capacity(value.len());
    for ch in value.chars() {
        match ch {
            '&' => escaped.push_str("&amp;"),
            '<' => escaped.push_str("&lt;"),
            '>' => escaped.push_str("&gt;"),
            '"' => escaped.push_str("&quot;"),
            '\'' => escaped.push_str("&apos;"),
            _ => escaped.push(ch),
        }
    }
    escaped
}

fn append_hook_events(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    source: HookHandlerSource<'_>,
    hook_events: HookEventsToml,
    policy: HookDiscoveryPolicy,
) {
    if !policy.allows(&source) {
        return;
    }

    for (event_name, groups) in hook_events.into_matcher_groups() {
        append_matcher_groups(
            handlers,
            hook_entries,
            warnings,
            display_order,
            &source,
            event_name,
            groups,
        );
    }
}

fn append_matcher_groups(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    source: &HookHandlerSource<'_>,
    event_name: codex_protocol::protocol::HookEventName,
    groups: Vec<MatcherGroup>,
) {
    for (group_index, group) in groups.into_iter().enumerate() {
        let matcher = matcher_pattern_for_event(event_name, group.matcher.as_deref());
        if let Some(matcher) = matcher
            && let Err(err) = validate_matcher_pattern(matcher)
        {
            warnings.push(format!(
                "invalid matcher {matcher:?} in {}: {err}",
                source.path.display()
            ));
            continue;
        }
        for (handler_index, handler) in group.hooks.iter().cloned().enumerate() {
            match handler {
                HookHandlerConfig::Command {
                    command,
                    command_windows,
                    timeout_sec,
                    r#async,
                    status_message,
                } => {
                    let command = if cfg!(windows) {
                        command_windows.unwrap_or(command)
                    } else {
                        command
                    };
                    if r#async {
                        warnings.push(format!(
                            "skipping async hook in {}: async hooks are not supported yet",
                            source.path.display()
                        ));
                        continue;
                    }
                    if command.trim().is_empty() {
                        warnings.push(format!(
                            "skipping empty hook command in {}",
                            source.path.display()
                        ));
                        continue;
                    }
                    let timeout_sec = timeout_sec.unwrap_or(600).max(1);
                    let normalized_handler = HookHandlerConfig::Command {
                        command: command.clone(),
                        command_windows: None,
                        timeout_sec: Some(timeout_sec),
                        r#async,
                        status_message: status_message.clone(),
                    };
                    let current_hash =
                        command_hook_hash(event_name, matcher, &group, normalized_handler);
                    let command = source.env.iter().fold(command, |command, (key, value)| {
                        command.replace(&format!("${{{key}}}"), value)
                    });
                    // TODO(abhinav): replace this positional suffix with a durable hook id.
                    let key =
                        crate::hook_key(&source.key_source, event_name, group_index, handler_index);
                    let state = source.hook_states.get(&key);
                    let enabled = hook_enabled(source.is_managed, state);
                    let trusted_hash = hook_trusted_hash(source.is_managed, state);
                    let trust_status =
                        hook_trust_status(source.is_managed, &current_hash, trusted_hash);
                    hook_entries.push(HookListEntry {
                        key,
                        event_name,
                        handler_type: HookHandlerType::Command,
                        matcher: matcher.map(ToOwned::to_owned),
                        command: Some(command.clone()),
                        timeout_sec,
                        status_message: status_message.clone(),
                        source_path: source.path.clone(),
                        source: source.source,
                        plugin_id: source.plugin_id.clone(),
                        display_order: *display_order,
                        enabled,
                        is_managed: source.is_managed,
                        current_hash,
                        trust_status,
                    });
                    if enabled
                        && (source.bypass_hook_trust
                            || matches!(
                                trust_status,
                                HookTrustStatus::Managed | HookTrustStatus::Trusted
                            ))
                    {
                        handlers.push(ConfiguredHandler {
                            event_name,
                            matcher: matcher.map(ToOwned::to_owned),
                            command,
                            timeout_sec,
                            status_message,
                            source_path: source.path.clone(),
                            source: source.source,
                            display_order: *display_order,
                            env: source.env.clone(),
                        });
                    }
                    *display_order += 1;
                }
                HookHandlerConfig::Prompt {} => warnings.push(format!(
                    "skipping prompt hook in {}: prompt hooks are not supported yet",
                    source.path.display()
                )),
                HookHandlerConfig::Agent {} => warnings.push(format!(
                    "skipping agent hook in {}: agent hooks are not supported yet",
                    source.path.display()
                )),
            }
        }
    }
}

/// Hash a normalized, config-derived identity instead of source text so equivalent
/// hooks from config TOML and hooks.json converge on the same trust identity.
#[derive(Serialize)]
struct NormalizedHookIdentity {
    event_name: &'static str,
    #[serde(flatten)]
    group: MatcherGroup,
}

fn command_hook_hash(
    event_name: codex_protocol::protocol::HookEventName,
    matcher: Option<&str>,
    group: &MatcherGroup,
    normalized_handler: HookHandlerConfig,
) -> String {
    let mut group = group.clone();
    group.matcher = matcher.map(ToOwned::to_owned);
    group.hooks = vec![normalized_handler];
    let identity = NormalizedHookIdentity {
        event_name: crate::hook_event_key_label(event_name),
        group,
    };
    let Ok(value) = TomlValue::try_from(identity) else {
        unreachable!("normalized hook identity should serialize to TOML");
    };
    version_for_toml(&value)
}

fn hook_trust_status(
    is_managed: bool,
    current_hash: &str,
    trusted_hash: Option<&str>,
) -> HookTrustStatus {
    if is_managed {
        HookTrustStatus::Managed
    } else {
        match trusted_hash {
            Some(trusted_hash) if trusted_hash == current_hash => HookTrustStatus::Trusted,
            Some(_) => HookTrustStatus::Modified,
            None => HookTrustStatus::Untrusted,
        }
    }
}

fn hook_enabled(is_managed: bool, state: Option<&HookStateToml>) -> bool {
    is_managed || state.and_then(|state| state.enabled) != Some(false)
}

fn hook_trusted_hash(is_managed: bool, state: Option<&HookStateToml>) -> Option<&str> {
    (!is_managed)
        .then(|| state.and_then(|state| state.trusted_hash.as_deref()))
        .flatten()
}

fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSource, bool) {
    match source {
        ConfigLayerSource::System { .. } => (HookSource::System, true),
        ConfigLayerSource::User { .. } => (HookSource::User, false),
        ConfigLayerSource::Project { .. } => (HookSource::Project, false),
        ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true),
        ConfigLayerSource::EnterpriseManaged { .. } => (HookSource::CloudManagedConfig, true),
        ConfigLayerSource::SessionFlags => (HookSource::SessionFlags, false),
        ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => {
            (HookSource::LegacyManagedConfigFile, true)
        }
        ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
            (HookSource::LegacyManagedConfigMdm, true)
        }
    }
}

fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> HookSource {
    match source {
        Some(RequirementSource::MdmManagedPreferences { .. }) => HookSource::Mdm,
        Some(RequirementSource::SystemRequirementsToml { .. }) => HookSource::System,
        Some(RequirementSource::LegacyManagedConfigTomlFromFile { .. }) => {
            HookSource::LegacyManagedConfigFile
        }
        Some(RequirementSource::LegacyManagedConfigTomlFromMdm) => {
            HookSource::LegacyManagedConfigMdm
        }
        Some(RequirementSource::Composite { sources }) => {
            // Requirements hook composition preserves contributing sources in
            // priority order, but discovery only carries one source for the
            // whole merged hooks field. Use the primary contributor as the best
            // available coarse attribution.
            hook_source_for_requirement_source(sources.first())
        }
        Some(RequirementSource::EnterpriseManaged { .. }) => HookSource::CloudRequirements,
        Some(RequirementSource::Unknown) | None => HookSource::Unknown,
    }
}

#[cfg(test)]
mod tests {
    use codex_config::ConfigLayerEntry;
    use codex_config::ConfigLayerSource;
    use codex_config::HookEventsToml;
    use codex_config::RequirementSource;
    use codex_protocol::protocol::HookEventName;
    use codex_protocol::protocol::HookSource;
    use codex_utils_absolute_path::AbsolutePathBuf;
    use codex_utils_absolute_path::test_support::PathBufExt;
    use codex_utils_absolute_path::test_support::test_path_buf;
    use pretty_assertions::assert_eq;

    use super::ConfiguredHandler;
    use super::append_matcher_groups;
    use codex_config::HookHandlerConfig;
    use codex_config::HookStateToml;
    use codex_config::MatcherGroup;
    use codex_config::TomlValue;
    use codex_protocol::protocol::HookTrustStatus;

    fn source_path() -> AbsolutePathBuf {
        test_path_buf("/tmp/hooks.json").abs()
    }

    fn hook_source() -> HookSource {
        HookSource::System
    }

    fn hook_handler_source<'a>(
        path: &'a AbsolutePathBuf,
        hook_states: &'a std::collections::HashMap<String, HookStateToml>,
    ) -> super::HookHandlerSource<'a> {
        super::HookHandlerSource {
            path,
            key_source: path.display().to_string(),
            source: hook_source(),
            is_managed: true,
            bypass_hook_trust: false,
            hook_states,
            env: std::collections::HashMap::new(),
            plugin_id: None,
        }
    }

    fn unmanaged_hook_handler_source<'a>(
        path: &'a AbsolutePathBuf,
        hook_states: &'a std::collections::HashMap<String, HookStateToml>,
        bypass_hook_trust: bool,
    ) -> super::HookHandlerSource<'a> {
        super::HookHandlerSource {
            path,
            key_source: path.display().to_string(),
            source: HookSource::User,
            is_managed: false,
            bypass_hook_trust,
            hook_states,
            env: std::collections::HashMap::new(),
            plugin_id: None,
        }
    }

    #[test]
    fn composite_requirement_hook_source_uses_primary_source() {
        let source = RequirementSource::Composite {
            sources: vec![
                RequirementSource::SystemRequirementsToml {
                    file: test_path_buf("/etc/codex/requirements.toml").abs(),
                },
                RequirementSource::EnterpriseManaged {
                    id: "layer-1".to_string(),
                    name: "Engineering".to_string(),
                },
            ],
        };

        assert_eq!(
            super::hook_source_for_requirement_source(Some(&source)),
            HookSource::System
        );
    }

    #[test]
    fn enterprise_managed_synthetic_path_escapes_display_fields() {
        let source = RequirementSource::EnterpriseManaged {
            id: "id<&>".to_string(),
            name: "Name <Admin> & \"Ops\"".to_string(),
        };

        let source_path = super::fallback_managed_hooks_source_path(Some(&source));
        let source_path = source_path.display().to_string();

        assert!(source_path.contains("Name &lt;Admin&gt; &amp; &quot;Ops&quot;"));
        assert!(source_path.contains("id&lt;&amp;&gt;"));
        assert!(!source_path.contains("Name <Admin>"));
    }

    fn command_group(matcher: Option<&str>) -> MatcherGroup {
        MatcherGroup {
            matcher: matcher.map(str::to_string),
            hooks: vec![HookHandlerConfig::Command {
                command: "echo hello".to_string(),
                command_windows: None,
                timeout_sec: None,
                r#async: false,
                status_message: None,
            }],
        }
    }

    #[test]
    fn user_prompt_submit_ignores_invalid_matcher_during_discovery() {
        let mut handlers = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut Vec::new(),
            &mut warnings,
            &mut display_order,
            &hook_handler_source(&source_path, &hook_states),
            HookEventName::UserPromptSubmit,
            vec![command_group(Some("["))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(
            handlers,
            vec![ConfiguredHandler {
                event_name: HookEventName::UserPromptSubmit,
                matcher: None,
                command: "echo hello".to_string(),
                timeout_sec: 600,
                status_message: None,
                source_path: source_path.clone(),
                source: hook_source(),
                display_order: 0,
                env: std::collections::HashMap::new(),
            }]
        );
    }

    #[test]
    fn pre_tool_use_keeps_valid_matcher_during_discovery() {
        let mut handlers = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut Vec::new(),
            &mut warnings,
            &mut display_order,
            &hook_handler_source(&source_path, &hook_states),
            HookEventName::PreToolUse,
            vec![command_group(Some("^Bash$"))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(
            handlers,
            vec![ConfiguredHandler {
                event_name: HookEventName::PreToolUse,
                matcher: Some("^Bash$".to_string()),
                command: "echo hello".to_string(),
                timeout_sec: 600,
                status_message: None,
                source_path: source_path.clone(),
                source: hook_source(),
                display_order: 0,
                env: std::collections::HashMap::new(),
            }]
        );
    }

    #[test]
    fn bypass_hook_trust_allows_enabled_untrusted_handlers() {
        let mut handlers = Vec::new();
        let mut hook_entries = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut hook_entries,
            &mut warnings,
            &mut display_order,
            &unmanaged_hook_handler_source(
                &source_path,
                &hook_states,
                /*bypass_hook_trust*/ true,
            ),
            HookEventName::PreToolUse,
            vec![command_group(Some("Bash"))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(handlers.len(), 1);
        assert_eq!(hook_entries.len(), 1);
        assert_eq!(hook_entries[0].trust_status, HookTrustStatus::Untrusted);
        assert_eq!(hook_entries[0].enabled, true);
    }

    #[test]
    fn bypass_hook_trust_respects_disabled_handlers() {
        let mut handlers = Vec::new();
        let mut hook_entries = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::from([(
            format!("{}:pre_tool_use:0:0", source_path.display()),
            HookStateToml {
                enabled: Some(false),
                trusted_hash: None,
            },
        )]);

        append_matcher_groups(
            &mut handlers,
            &mut hook_entries,
            &mut warnings,
            &mut display_order,
            &unmanaged_hook_handler_source(
                &source_path,
                &hook_states,
                /*bypass_hook_trust*/ true,
            ),
            HookEventName::PreToolUse,
            vec![command_group(Some("Bash"))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(handlers, Vec::<ConfiguredHandler>::new());
        assert_eq!(hook_entries.len(), 1);
        assert_eq!(hook_entries[0].trust_status, HookTrustStatus::Untrusted);
        assert_eq!(hook_entries[0].enabled, false);
    }

    #[test]
    fn pre_tool_use_treats_star_matcher_as_match_all() {
        let mut handlers = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut Vec::new(),
            &mut warnings,
            &mut display_order,
            &hook_handler_source(&source_path, &hook_states),
            HookEventName::PreToolUse,
            vec![command_group(Some("*"))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(handlers.len(), 1);
        assert_eq!(handlers[0].matcher.as_deref(), Some("*"));
    }

    #[test]
    fn post_tool_use_keeps_valid_matcher_during_discovery() {
        let mut handlers = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut Vec::new(),
            &mut warnings,
            &mut display_order,
            &hook_handler_source(&source_path, &hook_states),
            HookEventName::PostToolUse,
            vec![command_group(Some("Edit|Write"))],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(handlers.len(), 1);
        assert_eq!(handlers[0].event_name, HookEventName::PostToolUse);
        assert_eq!(handlers[0].matcher.as_deref(), Some("Edit|Write"));
    }

    #[test]
    fn toml_hook_discovery_ignores_malformed_state_entries() {
        let layer = ConfigLayerEntry::new(
            ConfigLayerSource::User {
                file: test_path_buf("/tmp/config.toml").abs(),
                profile: None,
            },
            config_with_malformed_state_and_session_start_hook(),
        );
        let mut warnings = Vec::new();

        let (_, hooks) = super::load_toml_hooks_from_layer(&layer, &mut warnings)
            .expect("valid hook events should still load");

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(
            hooks,
            HookEventsToml {
                session_start: vec![MatcherGroup {
                    matcher: None,
                    hooks: vec![HookHandlerConfig::Command {
                        command: "echo hello".to_string(),
                        command_windows: None,
                        timeout_sec: None,
                        r#async: false,
                        status_message: None,
                    }],
                }],
                ..Default::default()
            }
        );
    }

    #[test]
    fn pre_tool_use_resolves_windows_command_override_during_discovery() {
        let mut handlers = Vec::new();
        let mut warnings = Vec::new();
        let mut display_order = 0;
        let source_path = source_path();
        let hook_states = std::collections::HashMap::new();

        append_matcher_groups(
            &mut handlers,
            &mut Vec::new(),
            &mut warnings,
            &mut display_order,
            &hook_handler_source(&source_path, &hook_states),
            HookEventName::PreToolUse,
            vec![MatcherGroup {
                matcher: Some("^Bash$".to_string()),
                hooks: vec![HookHandlerConfig::Command {
                    command: "echo unix".to_string(),
                    command_windows: Some("echo windows".to_string()),
                    timeout_sec: None,
                    r#async: false,
                    status_message: None,
                }],
            }],
        );

        assert_eq!(warnings, Vec::<String>::new());
        assert_eq!(handlers.len(), 1);
        assert_eq!(
            handlers[0].command,
            if cfg!(windows) {
                "echo windows"
            } else {
                "echo unix"
            }
        );
    }

    fn config_with_malformed_state_and_session_start_hook() -> TomlValue {
        serde_json::from_value(serde_json::json!({
            "hooks": {
                "state": {
                    "some_key": {
                        "enabled": "not a bool",
                    },
                },
                "SessionStart": [{
                    "hooks": [{
                        "type": "command",
                        "command": "echo hello",
                    }],
                }],
            },
        }))
        .expect("config TOML should deserialize")
    }

    #[test]
    fn hook_metadata_for_config_layer_source_discards_source_details() {
        let config_file = test_path_buf("/tmp/.codex/config.toml").abs();
        let dot_codex_folder = test_path_buf("/tmp/worktree/.codex").abs();

        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::System {
                file: config_file.clone(),
            }),
            (HookSource::System, true),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::User {
                file: config_file.clone(),
                profile: None,
            }),
            (HookSource::User, false),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::Project {
                dot_codex_folder
            }),
            (HookSource::Project, false),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::Mdm {
                domain: "com.openai.codex".to_string(),
                key: "config".to_string(),
            }),
            (HookSource::Mdm, true),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::EnterpriseManaged {
                id: "cfg_123".to_string(),
                name: "Base policy".to_string(),
            }),
            (HookSource::CloudManagedConfig, true),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(&ConfigLayerSource::SessionFlags),
            (HookSource::SessionFlags, false),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(
                &ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: config_file },
            ),
            (HookSource::LegacyManagedConfigFile, true),
        );
        assert_eq!(
            super::hook_metadata_for_config_layer_source(
                &ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
            ),
            (HookSource::LegacyManagedConfigMdm, true),
        );
    }
}
