use super::parser::SessionCommandParser;
use super::service::SessionCommandService;
use super::types::{
    CommandBackend, CommandCapability, CommandContext, CommandPermission, RawSlashInvocation,
    SessionCommandFailure, SessionCommandFailureKind, SessionCommandOutcome,
    SlashCommandArgumentShape, SlashCommandDescriptor, SlashCommandHandler,
    SlashCommandRegistration, SlashCommandRequirements, SlashInvocation, SlashRegistryError,
};
use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock};

/// Central slash-command registry for runtime ingress paths.
///
/// The registry is the single runtime abstraction responsible for slash-command
/// registration, metadata discovery, name/alias resolution, argument-shape
/// validation, requirement checks, and dispatch into command handlers. CLI,
/// gateway, webhook, and channel ingress should call through this registry
/// instead of matching on slash command names directly.
pub struct SlashCommandRegistry {
    registrations: Vec<SlashCommandRegistration>,
    by_canonical_name: BTreeMap<&'static str, usize>,
    by_alias: BTreeMap<&'static str, usize>,
}

impl Default for SlashCommandRegistry {
    fn default() -> Self {
        Self::empty()
    }
}

impl SlashCommandRegistry {
    pub fn empty() -> Self {
        Self {
            registrations: Vec::new(),
            by_canonical_name: BTreeMap::new(),
            by_alias: BTreeMap::new(),
        }
    }

    pub fn register(
        &mut self,
        registration: SlashCommandRegistration,
    ) -> Result<(), SlashRegistryError> {
        validate_name(registration.descriptor.canonical_name)?;

        if registration.descriptor.description.trim().is_empty() {
            return Err(SlashRegistryError::EmptyDescription {
                canonical_name: registration.descriptor.canonical_name.to_string(),
            });
        }

        if self
            .by_canonical_name
            .contains_key(registration.descriptor.canonical_name)
        {
            return Err(SlashRegistryError::DuplicateCanonicalName {
                canonical_name: registration.descriptor.canonical_name.to_string(),
            });
        }

        for alias in registration.descriptor.aliases {
            validate_name(alias)?;

            if *alias == registration.descriptor.canonical_name {
                return Err(SlashRegistryError::AliasCollidesWithCanonical {
                    alias: alias.to_string(),
                    canonical_name: registration.descriptor.canonical_name.to_string(),
                });
            }

            if let Some(existing_index) = self.by_alias.get(alias) {
                return Err(SlashRegistryError::DuplicateAlias {
                    alias: alias.to_string(),
                    existing_canonical_name: self.registrations[*existing_index]
                        .descriptor
                        .canonical_name
                        .to_string(),
                });
            }

            if self.by_canonical_name.contains_key(alias) {
                return Err(SlashRegistryError::AliasCollidesWithCanonical {
                    alias: alias.to_string(),
                    canonical_name: alias.to_string(),
                });
            }
        }

        // Check canonical name doesn't conflict with existing alias
        if let Some(existing_index) = self.by_alias.get(registration.descriptor.canonical_name) {
            return Err(SlashRegistryError::AliasCollidesWithCanonical {
                alias: registration.descriptor.canonical_name.to_string(),
                canonical_name: self.registrations[*existing_index]
                    .descriptor
                    .canonical_name
                    .to_string(),
            });
        }

        // Detect duplicate aliases inside the same descriptor
        let mut seen_aliases = std::collections::HashSet::new();
        for alias in registration.descriptor.aliases {
            if !seen_aliases.insert(alias.to_string()) {
                return Err(SlashRegistryError::DuplicateAlias {
                    alias: alias.to_string(),
                    existing_canonical_name: registration.descriptor.canonical_name.to_string(),
                });
            }
        }

        let index = self.registrations.len();
        self.by_canonical_name
            .insert(registration.descriptor.canonical_name, index);
        for alias in registration.descriptor.aliases {
            self.by_alias.insert(alias, index);
        }
        self.registrations.push(registration);
        Ok(())
    }

    pub fn get(&self, name: &str) -> Option<&SlashCommandDescriptor> {
        self.resolve_registration(name)
            .map(|registration| &registration.descriptor)
    }

    pub fn contains(&self, name: &str) -> bool {
        self.resolve_registration(name).is_some()
    }

    pub fn iter(&self) -> impl Iterator<Item = &SlashCommandDescriptor> {
        self.registrations
            .iter()
            .map(|registration| &registration.descriptor)
    }

    pub async fn dispatch_prompt(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        prompt: &str,
    ) -> Option<SessionCommandOutcome> {
        let raw = SessionCommandParser::parse(prompt)?;
        self.resolve_registration(&raw.invoked_name)?;
        Some(self.dispatch_raw(service, context, raw).await)
    }

    pub async fn dispatch_raw(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        raw: RawSlashInvocation,
    ) -> SessionCommandOutcome {
        let Some(registration) = self.resolve_registration(&raw.invoked_name) else {
            return SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/",
                kind: SessionCommandFailureKind::UnknownCommand,
                session_id: Some(context.session.session_id.clone()),
                message: format!("unknown slash command: {}", raw.invoked_name),
            });
        };
        let invocation = match validate_invocation(&registration.descriptor, raw) {
            Ok(invocation) => invocation,
            Err(error) => return SessionCommandOutcome::Failure(error),
        };
        if let Err(error) = validate_requirements(&registration.descriptor, &context) {
            return SessionCommandOutcome::Failure(error);
        }

        registration
            .handler
            .handle(service, context, invocation)
            .await
    }

    fn resolve_registration(&self, name: &str) -> Option<&SlashCommandRegistration> {
        self.by_canonical_name
            .get(name)
            .or_else(|| self.by_alias.get(name))
            .and_then(|index| self.registrations.get(*index))
    }
}

pub fn default_registry() -> &'static SlashCommandRegistry {
    static REGISTRY: OnceLock<SlashCommandRegistry> = OnceLock::new();
    REGISTRY.get_or_init(build_default_registry)
}

fn build_default_registry() -> SlashCommandRegistry {
    let mut registry = SlashCommandRegistry::empty();
    for registration in built_in_registrations() {
        if let Err(error) = registry.register(registration) {
            panic!("invalid built-in slash command registry: {error:?}");
        }
    }
    registry
}

fn built_in_registrations() -> [SlashCommandRegistration; 11] {
    const NO_CAPABILITIES: &[CommandCapability] = &[];
    const SESSION_LIFECYCLE: &[CommandCapability] = &[CommandCapability::SessionLifecycle];
    const SESSION_SUMMARY: &[CommandCapability] = &[CommandCapability::SessionSummary];
    const SESSION_READ: &[CommandCapability] = &[CommandCapability::SessionRead];
    const SETTINGS_READ: &[CommandCapability] = &[CommandCapability::SettingsRead];
    const MCP_MANAGEMENT: &[CommandCapability] = &[CommandCapability::McpManagement];
    const TOOL_MANAGEMENT: &[CommandCapability] = &[CommandCapability::ToolManagement];
    const RESUME_PERMISSIONS: &[CommandPermission] = &[
        CommandPermission::RequiresCallerScope,
        CommandPermission::RequiresResumableSessionVisibility,
    ];
    const NO_PERMISSIONS: &[CommandPermission] = &[];
    const SQLITE_BACKEND: &[CommandBackend] = &[CommandBackend::SqliteSlashSessions];

    [
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/tools",
                aliases: &[],
                description: "List the effective tools available in this runtime.",
                argument_shape: SlashCommandArgumentShape::None,
                requirements: SlashCommandRequirements {
                    capabilities: NO_CAPABILITIES,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(ToolsHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/resume",
                aliases: &["/continue"],
                description: "Resume a suspended session or list resumable sessions.",
                argument_shape: SlashCommandArgumentShape::OptionalTargetThenText,
                requirements: SlashCommandRequirements {
                    capabilities: SESSION_LIFECYCLE,
                    permissions: RESUME_PERMISSIONS,
                    backends: SQLITE_BACKEND,
                },
            },
            handler: Arc::new(ResumeHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/suspend",
                aliases: &[],
                description: "Suspend the current session using the latest compact snapshot.",
                argument_shape: SlashCommandArgumentShape::None,
                requirements: SlashCommandRequirements {
                    capabilities: SESSION_LIFECYCLE,
                    permissions: NO_PERMISSIONS,
                    backends: SQLITE_BACKEND,
                },
            },
            handler: Arc::new(SuspendHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/tldr",
                aliases: &[],
                description: "Persist a concise session summary snapshot.",
                argument_shape: SlashCommandArgumentShape::None,
                requirements: SlashCommandRequirements {
                    capabilities: SESSION_SUMMARY,
                    permissions: NO_PERMISSIONS,
                    backends: SQLITE_BACKEND,
                },
            },
            handler: Arc::new(TldrHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/compact",
                aliases: &[],
                description: "Create a resume-capable compact snapshot for the current session.",
                argument_shape: SlashCommandArgumentShape::OptionalText,
                requirements: SlashCommandRequirements {
                    capabilities: SESSION_SUMMARY,
                    permissions: NO_PERMISSIONS,
                    backends: SQLITE_BACKEND,
                },
            },
            handler: Arc::new(CompactHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/session",
                aliases: &[],
                description:
                    "Show session help, caller-scoped list, compact status, or richer inspection.",
                argument_shape: SlashCommandArgumentShape::OptionalText,
                requirements: SlashCommandRequirements {
                    capabilities: SESSION_READ,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(SessionHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/model",
                aliases: &[],
                description: "Show or set the active model for this session.",
                argument_shape: SlashCommandArgumentShape::OptionalText,
                requirements: SlashCommandRequirements {
                    capabilities: SETTINGS_READ,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(ModelHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/provider",
                aliases: &[],
                description: "Show or set the active provider for this session.",
                argument_shape: SlashCommandArgumentShape::OptionalText,
                requirements: SlashCommandRequirements {
                    capabilities: SETTINGS_READ,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(ProviderHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/temperature",
                aliases: &[],
                description: "Show the current sampling temperature for this session.",
                argument_shape: SlashCommandArgumentShape::None,
                requirements: SlashCommandRequirements {
                    capabilities: SETTINGS_READ,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(TemperatureHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/mcp",
                aliases: &[],
                description: "Manage MCP servers: list, add <name>, remove <name>.",
                argument_shape: SlashCommandArgumentShape::RequiredText,
                requirements: SlashCommandRequirements {
                    capabilities: MCP_MANAGEMENT,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(McpHandler),
        },
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name: "/tool",
                aliases: &[],
                description: "Enable or disable a tool: enable <name>, disable <name>.",
                argument_shape: SlashCommandArgumentShape::RequiredText,
                requirements: SlashCommandRequirements {
                    capabilities: TOOL_MANAGEMENT,
                    permissions: NO_PERMISSIONS,
                    backends: &[],
                },
            },
            handler: Arc::new(ToolManageHandler),
        },
    ]
}

struct ToolsHandler;
struct ResumeHandler;
struct SuspendHandler;
struct TldrHandler;
struct CompactHandler;
struct SessionHandler;
struct ModelHandler;
struct ProviderHandler;
struct TemperatureHandler;
struct McpHandler;
struct ToolManageHandler;

#[async_trait::async_trait]
impl SlashCommandHandler for ToolsHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        _invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_tools(&context.session.session_id)
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for ResumeHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service
            .handle_resume(&context, invocation.primary_target.as_deref())
            .await
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for SuspendHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        _invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_suspend(&context.session.session_id).await
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for TldrHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        _invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_tldr(&context.session.session_id).await
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for CompactHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service
            .handle_compact(&context.session.session_id, &invocation.raw_args)
            .await
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for SessionHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_session(&context, &invocation.raw_args).await
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for ModelHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_model(&context.session.session_id, &invocation.raw_args)
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for ProviderHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_provider(&context.session.session_id, &invocation.raw_args)
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for TemperatureHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        _invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_temperature(&context.session.session_id)
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for McpHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_mcp(&context.session.session_id, &invocation.raw_args)
    }
}

#[async_trait::async_trait]
impl SlashCommandHandler for ToolManageHandler {
    async fn handle(
        &self,
        service: &SessionCommandService<'_>,
        context: CommandContext,
        invocation: SlashInvocation,
    ) -> SessionCommandOutcome {
        service.handle_tool_manage(&context.session.session_id, &invocation.raw_args)
    }
}

fn validate_name(name: &str) -> Result<(), SlashRegistryError> {
    let chars = name.chars().collect::<Vec<_>>();
    let valid = chars.len() >= 2
        && chars[0] == '/'
        && chars[1].is_ascii_lowercase()
        && chars[1..]
            .iter()
            .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || *ch == '-');

    if valid {
        Ok(())
    } else {
        Err(SlashRegistryError::InvalidName {
            name: name.to_string(),
        })
    }
}

fn validate_requirements(
    descriptor: &SlashCommandDescriptor,
    context: &CommandContext,
) -> Result<(), SessionCommandFailure> {
    for permission in descriptor.requirements.permissions {
        match permission {
            CommandPermission::RequiresCallerScope if !context.facts.has_caller_scope => {
                return Err(SessionCommandFailure {
                    command: descriptor.canonical_name,
                    kind: SessionCommandFailureKind::MissingCallerScope,
                    session_id: Some(context.session.session_id.clone()),
                    message: "permission denied: caller scope unavailable".to_string(),
                });
            }
            CommandPermission::RequiresCallerScope
            | CommandPermission::RequiresResumableSessionVisibility => {}
        }
    }

    Ok(())
}

fn validate_invocation(
    descriptor: &SlashCommandDescriptor,
    raw: RawSlashInvocation,
) -> Result<SlashInvocation, SessionCommandFailure> {
    match descriptor.argument_shape {
        SlashCommandArgumentShape::None => {
            if raw.raw_args.is_empty() {
                Ok(SlashInvocation {
                    invoked_name: raw.invoked_name,
                    canonical_name: descriptor.canonical_name,
                    raw_args: String::new(),
                    primary_target: None,
                })
            } else {
                Err(SessionCommandFailure {
                    command: descriptor.canonical_name,
                    kind: SessionCommandFailureKind::InvalidArguments,
                    session_id: None,
                    message: format!(
                        "invalid slash command usage for {}: this command does not accept trailing arguments",
                        descriptor.canonical_name
                    ),
                })
            }
        }
        SlashCommandArgumentShape::OptionalText => Ok(SlashInvocation {
            invoked_name: raw.invoked_name,
            canonical_name: descriptor.canonical_name,
            raw_args: raw.raw_args,
            primary_target: None,
        }),
        SlashCommandArgumentShape::OptionalTargetThenText => {
            let (primary_target, raw_args) =
                SessionCommandParser::split_primary_target(&raw.raw_args);
            Ok(SlashInvocation {
                invoked_name: raw.invoked_name,
                canonical_name: descriptor.canonical_name,
                raw_args,
                primary_target,
            })
        }
        SlashCommandArgumentShape::RequiredText => {
            if raw.raw_args.trim().is_empty() {
                Err(SessionCommandFailure {
                    command: descriptor.canonical_name,
                    kind: SessionCommandFailureKind::InvalidArguments,
                    session_id: None,
                    message: format!(
                        "invalid slash command usage for {}: a subcommand argument is required",
                        descriptor.canonical_name
                    ),
                })
            } else {
                Ok(SlashInvocation {
                    invoked_name: raw.invoked_name,
                    canonical_name: descriptor.canonical_name,
                    raw_args: raw.raw_args,
                    primary_target: None,
                })
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ExecutionMode;
    use crate::memory::{Memory, MemoryCategory, MemoryEntry};
    use crate::session_commands::{CommandSessionSource, SessionCommandOutcome};
    use async_trait::async_trait;
    use std::sync::atomic::{AtomicUsize, Ordering};

    struct NoopHandler;

    #[async_trait]
    impl SlashCommandHandler for NoopHandler {
        async fn handle(
            &self,
            _service: &SessionCommandService<'_>,
            _context: CommandContext,
            _invocation: SlashInvocation,
        ) -> SessionCommandOutcome {
            unreachable!("handler should not run in registry validation tests")
        }
    }

    fn registration(
        canonical_name: &'static str,
        aliases: &'static [&'static str],
        description: &'static str,
        argument_shape: SlashCommandArgumentShape,
    ) -> SlashCommandRegistration {
        SlashCommandRegistration {
            descriptor: SlashCommandDescriptor {
                canonical_name,
                aliases,
                description,
                argument_shape,
                requirements: SlashCommandRequirements::default(),
            },
            handler: Arc::new(NoopHandler),
        }
    }

    #[test]
    fn registry_rejects_invalid_names() {
        let mut registry = SlashCommandRegistry::empty();

        let error = registry
            .register(registration(
                "/Resume",
                &[],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::InvalidName {
                name: "/Resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_empty_descriptions() {
        let mut registry = SlashCommandRegistry::empty();

        let error = registry
            .register(registration(
                "/resume",
                &[],
                "   ",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::EmptyDescription {
                canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_duplicate_canonical_names() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/resume",
                &[],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        let error = registry
            .register(registration(
                "/resume",
                &[],
                "resume again",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::DuplicateCanonicalName {
                canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_duplicate_aliases() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/resume",
                &["/continue"],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        let error = registry
            .register(registration(
                "/compact",
                &["/continue"],
                "compact a session",
                SlashCommandArgumentShape::OptionalText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::DuplicateAlias {
                alias: "/continue".to_string(),
                existing_canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_duplicate_aliases_within_one_descriptor() {
        let mut registry = SlashCommandRegistry::empty();

        let error = registry
            .register(registration(
                "/resume",
                &["/continue", "/continue"],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::DuplicateAlias {
                alias: "/continue".to_string(),
                existing_canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_canonical_names_that_collide_with_existing_aliases() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/resume",
                &["/continue"],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        let error = registry
            .register(registration(
                "/continue",
                &[],
                "continue a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::AliasCollidesWithCanonical {
                alias: "/continue".to_string(),
                canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_rejects_alias_collisions_with_canonical_names() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/resume",
                &[],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        let error = registry
            .register(registration(
                "/compact",
                &["/resume"],
                "compact a session",
                SlashCommandArgumentShape::OptionalText,
            ))
            .unwrap_err();

        assert_eq!(
            error,
            SlashRegistryError::AliasCollidesWithCanonical {
                alias: "/resume".to_string(),
                canonical_name: "/resume".to_string(),
            }
        );
    }

    #[test]
    fn registry_supports_canonical_and_alias_lookup() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/resume",
                &["/continue"],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        assert_eq!(
            registry
                .get("/resume")
                .map(|descriptor| descriptor.canonical_name),
            Some("/resume")
        );
        assert_eq!(
            registry
                .get("/continue")
                .map(|descriptor| descriptor.canonical_name),
            Some("/resume")
        );
        assert!(registry.contains("/resume"));
        assert!(registry.contains("/continue"));
        assert!(!registry.contains("/missing"));
    }

    #[test]
    fn registry_iteration_exposes_discoverable_metadata_in_registration_order() {
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(registration(
                "/tools",
                &[],
                "list tools",
                SlashCommandArgumentShape::None,
            ))
            .unwrap();
        registry
            .register(registration(
                "/resume",
                &["/continue"],
                "resume a session",
                SlashCommandArgumentShape::OptionalTargetThenText,
            ))
            .unwrap();

        let metadata = registry
            .iter()
            .map(|descriptor| {
                (
                    descriptor.canonical_name,
                    descriptor.aliases,
                    descriptor.description,
                    descriptor.argument_shape.clone(),
                )
            })
            .collect::<Vec<_>>();

        assert_eq!(metadata.len(), 2);
        assert_eq!(metadata[0].0, "/tools");
        assert_eq!(metadata[0].1, &[] as &[&str]);
        assert_eq!(metadata[0].2, "list tools");
        assert_eq!(metadata[0].3, SlashCommandArgumentShape::None);
        assert_eq!(metadata[1].0, "/resume");
        assert_eq!(metadata[1].1, &["/continue"] as &[&str]);
        assert_eq!(metadata[1].2, "resume a session");
        assert_eq!(
            metadata[1].3,
            SlashCommandArgumentShape::OptionalTargetThenText
        );
    }

    #[test]
    fn default_registry_exposes_built_in_descriptors() {
        let registry = default_registry();
        let names = registry
            .iter()
            .map(|descriptor| descriptor.canonical_name)
            .collect::<Vec<_>>();
        let tools = registry
            .get("/tools")
            .expect("/tools descriptor should exist");
        let resume = registry
            .get("/resume")
            .expect("/resume descriptor should exist");
        let session = registry
            .get("/session")
            .expect("/session descriptor should exist");

        assert_eq!(
            names,
            vec![
                "/tools",
                "/resume",
                "/suspend",
                "/tldr",
                "/compact",
                "/session",
                "/model",
                "/provider",
                "/temperature",
                "/mcp",
                "/tool"
            ]
        );
        assert_eq!(tools.argument_shape, SlashCommandArgumentShape::None);
        assert_eq!(
            tools.description,
            "List the effective tools available in this runtime."
        );
        assert_eq!(
            Some(resume.argument_shape.clone()),
            Some(SlashCommandArgumentShape::OptionalTargetThenText)
        );
        assert_eq!(
            resume.description,
            "Resume a suspended session or list resumable sessions."
        );
        assert_eq!(
            resume.requirements.capabilities,
            &[CommandCapability::SessionLifecycle]
        );
        assert_eq!(
            resume.requirements.permissions,
            &[
                CommandPermission::RequiresCallerScope,
                CommandPermission::RequiresResumableSessionVisibility,
            ]
        );
        assert_eq!(
            resume.requirements.backends,
            &[CommandBackend::SqliteSlashSessions]
        );
        assert_eq!(
            session.argument_shape,
            SlashCommandArgumentShape::OptionalText
        );
        assert_eq!(
            session.requirements.capabilities,
            &[CommandCapability::SessionRead]
        );
        assert!(registry.get("/session status").is_none());
        assert!(registry.get("/session inspect").is_none());
        assert!(registry.get("/session list").is_none());

        let resume_alias = registry
            .get("/continue")
            .expect("/continue alias should resolve to /resume");
        assert_eq!(resume_alias.canonical_name, "/resume");

        assert_eq!(
            registry
                .get("/compact")
                .map(|descriptor| descriptor.argument_shape.clone()),
            Some(SlashCommandArgumentShape::OptionalText)
        );
    }

    #[tokio::test]
    async fn dispatch_raw_reports_unknown_registered_command() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_raw(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                RawSlashInvocation {
                    invoked_name: "/missing".to_string(),
                    raw_args: String::new(),
                },
            )
            .await;

        assert_eq!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/",
                kind: SessionCommandFailureKind::UnknownCommand,
                session_id: Some("session-1".to_string()),
                message: "unknown slash command: /missing".to_string(),
            })
        );
    }

    #[tokio::test]
    async fn dispatch_prompt_falls_through_unknown_slash_commands() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/missing",
            )
            .await;

        assert_eq!(result, None);
    }

    #[tokio::test]
    async fn dispatch_denies_command_when_declared_caller_scope_permission_is_missing() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/resume",
            )
            .await;

        assert_eq!(
            result,
            Some(SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/resume",
                kind: SessionCommandFailureKind::MissingCallerScope,
                session_id: Some("session-1".to_string()),
                message: "permission denied: caller scope unavailable".to_string(),
            }))
        );
    }

    #[tokio::test]
    async fn dispatch_validates_argument_shape_for_exact_commands() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/tldr extra args",
            )
            .await;

        assert!(matches!(
            result,
            Some(SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/tldr",
                kind: SessionCommandFailureKind::InvalidArguments,
                ..
            }))
        ));
    }

    #[tokio::test]
    async fn dispatch_validates_argument_shape_for_tools() {
        let tools = vec![crate::session_commands::SessionCommandToolEntry {
            name: "shell".to_string(),
            description: "Execute shell commands".to_string(),
            source_kind: crate::session_commands::SessionCommandToolSourceKind::Native,
            source_label: None,
            aliases: vec![],
        }];
        let service = SessionCommandService::with_tool_snapshot(&RegistryMemory, &tools);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/tools extra",
            )
            .await;

        assert!(matches!(
            result,
            Some(SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/tools",
                kind: SessionCommandFailureKind::InvalidArguments,
                ..
            }))
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_tools_via_shared_service_handler() {
        let tools = vec![crate::session_commands::SessionCommandToolEntry {
            name: "shell".to_string(),
            description: "Execute shell commands".to_string(),
            source_kind: crate::session_commands::SessionCommandToolSourceKind::Native,
            source_label: None,
            aliases: vec![],
        }];
        let service = SessionCommandService::with_tool_snapshot(&RegistryMemory, &tools);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/tools",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Success(crate::session_commands::SessionCommandSuccess {
                command: "/tools",
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_built_ins_to_existing_service_behavior() {
        let memory = CountingRegistryMemory::default();
        let service = SessionCommandService::new(&memory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/compact keep latest goals",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                kind: SessionCommandFailureKind::UnsupportedBackend,
                ..
            })
        ));
        assert!(memory.name_calls.load(Ordering::SeqCst) >= 1);
    }

    #[tokio::test]
    async fn dispatch_preserves_resume_authorization_after_registry_lookup() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let memory = crate::memory::SqliteMemory::new(temp_dir.path()).unwrap();
        let service = SessionCommandService::new(&memory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/resume",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                kind: SessionCommandFailureKind::MissingCallerScope,
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_resolves_alias_to_canonical_handler() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let memory = crate::memory::SqliteMemory::new(temp_dir.path()).unwrap();
        let service = SessionCommandService::new(&memory);
        let mut registry = SlashCommandRegistry::empty();
        registry
            .register(SlashCommandRegistration {
                descriptor: SlashCommandDescriptor {
                    canonical_name: "/resume",
                    aliases: &["/continue"],
                    description: "resume a session",
                    argument_shape: SlashCommandArgumentShape::OptionalTargetThenText,
                    requirements: SlashCommandRequirements::default(),
                },
                handler: Arc::new(ResumeHandler),
            })
            .unwrap();

        let result = registry
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/continue",
            )
            .await
            .expect("alias should resolve to canonical command");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/resume",
                kind: SessionCommandFailureKind::MissingCallerScope,
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_suspend_via_registry() {
        let memory = CountingRegistryMemory::default();
        let service = SessionCommandService::new(&memory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/suspend",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                kind: SessionCommandFailureKind::UnsupportedBackend,
                ..
            })
        ));
        assert!(memory.name_calls.load(Ordering::SeqCst) >= 1);
    }

    #[tokio::test]
    async fn dispatch_routes_session_root_help_with_empty_raw_args() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/session",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Success(crate::session_commands::SessionCommandSuccess {
                command: "/session",
                data: crate::session_commands::SessionCommandSuccessData::SessionHelp { .. },
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_session_status_as_raw_args_on_canonical_command() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/session status",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/session",
                kind: SessionCommandFailureKind::UnsupportedBackend,
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_session_inspect_as_raw_args_on_canonical_command() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/session inspect",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/session",
                kind: SessionCommandFailureKind::UnsupportedBackend,
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_routes_session_list_as_raw_args_on_canonical_command() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let memory = crate::memory::SqliteMemory::new(temp_dir.path()).unwrap();
        let service = SessionCommandService::new(&memory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    Some("scope-a".to_string()),
                ),
                "/session list",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Success(crate::session_commands::SessionCommandSuccess {
                command: "/session",
                data: crate::session_commands::SessionCommandSuccessData::SessionList { .. },
                ..
            })
        ));
    }

    #[tokio::test]
    async fn dispatch_keeps_unsupported_session_subcommands_inside_canonical_family_handler() {
        let service = SessionCommandService::new(&RegistryMemory);

        let result = default_registry()
            .dispatch_prompt(
                &service,
                CommandContext::for_cli(
                    "session-1",
                    CommandSessionSource::Existing,
                    ExecutionMode::Standard,
                    None,
                ),
                "/session archive",
            )
            .await
            .expect("built-in command should resolve");

        assert!(matches!(
            result,
            SessionCommandOutcome::Failure(SessionCommandFailure {
                command: "/session",
                kind: SessionCommandFailureKind::InvalidArguments,
                ..
            })
        ));
    }

    struct RegistryMemory;

    #[async_trait]
    impl Memory for RegistryMemory {
        fn name(&self) -> &str {
            "none"
        }

        async fn store(
            &self,
            _key: &str,
            _content: &str,
            _category: MemoryCategory,
            _session_id: Option<&str>,
        ) -> anyhow::Result<()> {
            Ok(())
        }

        async fn recall(
            &self,
            _query: &str,
            _limit: usize,
            _session_id: Option<&str>,
        ) -> anyhow::Result<Vec<MemoryEntry>> {
            Ok(Vec::new())
        }

        async fn get(&self, _key: &str) -> anyhow::Result<Option<MemoryEntry>> {
            Ok(None)
        }

        async fn list(
            &self,
            _category: Option<&MemoryCategory>,
            _session_id: Option<&str>,
        ) -> anyhow::Result<Vec<MemoryEntry>> {
            Ok(Vec::new())
        }

        async fn forget(&self, _key: &str) -> anyhow::Result<bool> {
            Ok(false)
        }

        async fn count(&self) -> anyhow::Result<usize> {
            Ok(0)
        }

        async fn health_check(&self) -> bool {
            true
        }
    }

    #[derive(Default)]
    struct CountingRegistryMemory {
        name_calls: AtomicUsize,
    }

    #[async_trait]
    impl Memory for CountingRegistryMemory {
        fn name(&self) -> &str {
            self.name_calls.fetch_add(1, Ordering::SeqCst);
            "none"
        }

        async fn store(
            &self,
            _key: &str,
            _content: &str,
            _category: MemoryCategory,
            _session_id: Option<&str>,
        ) -> anyhow::Result<()> {
            Ok(())
        }

        async fn recall(
            &self,
            _query: &str,
            _limit: usize,
            _session_id: Option<&str>,
        ) -> anyhow::Result<Vec<MemoryEntry>> {
            Ok(Vec::new())
        }

        async fn get(&self, _key: &str) -> anyhow::Result<Option<MemoryEntry>> {
            Ok(None)
        }

        async fn list(
            &self,
            _category: Option<&MemoryCategory>,
            _session_id: Option<&str>,
        ) -> anyhow::Result<Vec<MemoryEntry>> {
            Ok(Vec::new())
        }

        async fn forget(&self, _key: &str) -> anyhow::Result<bool> {
            Ok(false)
        }

        async fn count(&self) -> anyhow::Result<usize> {
            Ok(0)
        }

        async fn health_check(&self) -> bool {
            true
        }
    }
}
