//! Unified intent classification registry.
//!
//! Replaces the 22 scattered `requests_*()` functions in `intents.rs` and
//! 3 local intent detectors in `channel_message.rs` with a single
//! [`IntentRegistry::classify_semantic()`] entry point.
//!
//! ## Matcher strategy
//!
//! - [`IntentMatcher::Syntactic`] — structural / length-gated tests that run
//!   synchronously in `classify()`. Reserved for Contradiction, ShortFollowup,
//!   and ReactiveSarcasm (short-form channel intents that must not require an
//!   embedding round-trip).
//! - [`IntentMatcher::Semantic`] — delegated to [`SemanticClassifier`] via
//!   `classify_semantic()`. All user-intent categories use this path.

use std::collections::HashSet;

use roboticus_llm::semantic_classifier::SemanticClassifier;

/// Default cosine similarity threshold for semantic intent matching.
/// Intents whose centroid similarity to the input is below this value are
/// not reported. Calibrated against the n-gram fallback embedding.
pub(super) const INTENT_THRESHOLD: f64 = 0.60;

// ── Intent enum ──────────────────────────────────────────────────────────

/// All classifiable user intents. Each variant maps 1:1 to a former
/// `requests_*()` function or channel-local intent detector.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) enum Intent {
    /// Tool-use or script/command execution.
    Execution,
    /// Task management — closing, completing, implementing work items.
    TaskManagement,
    /// Explicit delegation to a subagent.
    Delegation,
    /// Cron job scheduling request.
    Cron,
    /// File distribution listing.
    FileDistribution,
    /// Folder scan / directory listing.
    FolderScan,
    /// "Pick a random tool and use it" requests.
    RandomToolUse,
    /// Model identity query ("/status", "what model").
    ModelIdentity,
    /// Current events / geopolitical sitrep.
    CurrentEvents,
    /// Introspection of available tools and subagents.
    Introspection,
    /// Terse acknowledgement + wait pattern.
    Acknowledgement,
    /// LLM provider inventory query.
    ProviderInventory,
    /// Personality / identity profile query.
    PersonalityProfile,
    /// Capability summary ("what can you do?").
    CapabilitySummary,
    /// Wallet address / credential scan in filesystem.
    WalletAddressScan,
    /// Image file count scan.
    ImageCountScan,
    /// Markdown file count scan.
    MarkdownCountScan,
    /// Obsidian vault insights.
    ObsidianInsights,
    /// Email triage / inbox check.
    EmailTriage,
    /// Literary quote contextualisation.
    LiteraryQuoteContext,
    /// Short contradiction follow-up ("that's not true", "incorrect").
    Contradiction,
    /// Short follow-up referencing a previous reply ("what's that from?").
    ShortFollowup,
    /// Short reactive sarcasm ("wow", "great", "sure").
    ReactiveSarcasm,
}

impl Intent {
    /// Lightweight synchronous check for a single intent.
    ///
    /// **DEPRECATED**: This is a keyword-based fallback that cannot detect
    /// intents semantically. New code should use pre-classified intents from
    /// `ctx.intents` or call `IntentRegistry::classify_semantic()` instead.
    /// Existing callers in `guard_fallback.rs` should be migrated to read
    /// from the pre-classified intent list when feasible.
    #[deprecated(
        note = "Use pre-classified intents from ctx.intents or classify_semantic() instead"
    )]
    pub fn matches(self, prompt: &str) -> bool {
        let lower = prompt.to_ascii_lowercase();
        match self {
            Self::ObsidianInsights => {
                (lower.contains("obsidian") || lower.contains("vault"))
                    && (lower.contains("insight")
                        || lower.contains("summary")
                        || lower.contains("what")
                        || lower.contains("say about"))
            }
            Self::CurrentEvents => {
                lower.contains("geopolit")
                    || lower.contains("sitrep")
                    || lower.contains("current events")
                    || lower.contains("latest news")
                    || lower.contains("what's happening")
            }
            Self::CapabilitySummary => {
                lower.contains("what are you able to do")
                    || lower.contains("what can you do")
                    || lower.contains("what can you help")
            }
            Self::PersonalityProfile => {
                (lower.contains("personality") && lower.contains("you"))
                    || lower.contains("who are you")
            }
            Self::ProviderInventory => {
                lower.contains("llm provider")
                    || lower.contains("which provider")
                    || lower.contains("what provider")
            }
            // For all other intents, fall back to the synchronous registry
            // (returns syntactic channel intents only).
            other => {
                let registry = IntentRegistry::default_registry();
                registry.classify(prompt).contains(&other)
            }
        }
    }

    /// Map a [`SemanticClassifier`] category name to an [`Intent`] variant.
    fn from_category(name: &str) -> Option<Self> {
        use roboticus_llm::intent_exemplars::*;
        match name {
            CAT_EXECUTION => Some(Self::Execution),
            CAT_TASK_MANAGEMENT => Some(Self::TaskManagement),
            CAT_DELEGATION => Some(Self::Delegation),
            CAT_CRON => Some(Self::Cron),
            CAT_FILE_DISTRIBUTION => Some(Self::FileDistribution),
            CAT_FOLDER_SCAN => Some(Self::FolderScan),
            CAT_RANDOM_TOOL_USE => Some(Self::RandomToolUse),
            CAT_MODEL_IDENTITY => Some(Self::ModelIdentity),
            CAT_CURRENT_EVENTS => Some(Self::CurrentEvents),
            CAT_INTROSPECTION => Some(Self::Introspection),
            CAT_ACKNOWLEDGEMENT => Some(Self::Acknowledgement),
            CAT_PROVIDER_INVENTORY => Some(Self::ProviderInventory),
            CAT_PERSONALITY_PROFILE => Some(Self::PersonalityProfile),
            CAT_CAPABILITY_SUMMARY => Some(Self::CapabilitySummary),
            CAT_WALLET_ADDRESS_SCAN => Some(Self::WalletAddressScan),
            CAT_IMAGE_COUNT_SCAN => Some(Self::ImageCountScan),
            CAT_MARKDOWN_COUNT_SCAN => Some(Self::MarkdownCountScan),
            CAT_OBSIDIAN_INSIGHTS => Some(Self::ObsidianInsights),
            CAT_EMAIL_TRIAGE => Some(Self::EmailTriage),
            CAT_LITERARY_QUOTE_CONTEXT => Some(Self::LiteraryQuoteContext),
            _ => None,
        }
    }
}

// ── IntentMatcher ────────────────────────────────────────────────────────

/// Matching strategy for a single intent descriptor.
pub(super) enum IntentMatcher {
    /// Structural / length-gated test. Receives the already-lowercased prompt.
    /// Used only for short-form channel intents (Contradiction, ShortFollowup,
    /// ReactiveSarcasm) that require no embedding round-trip.
    Syntactic(fn(&str) -> bool),
    /// Delegated to [`SemanticClassifier`] via `classify_semantic()`.
    Semantic,
}

// ── IntentDescriptor ─────────────────────────────────────────────────────

/// Descriptor binding an intent to its classification metadata.
pub(super) struct IntentDescriptor {
    pub intent: Intent,
    /// Shortcut dispatch priority (higher = checked first, wins conflicts).
    pub priority: u8,
    /// If true, cached responses are skipped when this intent is detected.
    pub bypasses_cache: bool,
    /// The matching strategy for this intent.
    pub matcher: IntentMatcher,
}

// ── IntentRegistry ───────────────────────────────────────────────────────

/// Central registry for all intent classification.
///
/// - `classify()` — synchronous, returns only [`IntentMatcher::Syntactic`]
///   matches. Used for the three short-form channel intents.
/// - `classify_semantic()` — async, runs the [`SemanticClassifier`] and merges
///   with syntactic results. Call sites must use this method.
pub(super) struct IntentRegistry {
    descriptors: Vec<IntentDescriptor>,
}

impl IntentRegistry {
    /// Build the default registry with all known intents.
    pub fn default_registry() -> Self {
        Self {
            descriptors: vec![
                // ── Semantic intents ──────────────────────────────
                IntentDescriptor {
                    intent: Intent::Execution,
                    priority: 10,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::FileDistribution,
                    priority: 37,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::ModelIdentity,
                    priority: 80,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::CurrentEvents,
                    priority: 65,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::Introspection,
                    priority: 60,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::ProviderInventory,
                    priority: 75,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::CapabilitySummary,
                    priority: 71,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::FolderScan,
                    priority: 39,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::Acknowledgement,
                    priority: 85,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::WalletAddressScan,
                    priority: 45,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::ImageCountScan,
                    priority: 43,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::MarkdownCountScan,
                    priority: 41,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::ObsidianInsights,
                    priority: 35,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::Delegation,
                    priority: 55,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::Cron,
                    priority: 53,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::RandomToolUse,
                    priority: 50,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::PersonalityProfile,
                    priority: 73,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::EmailTriage,
                    priority: 63,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                IntentDescriptor {
                    intent: Intent::LiteraryQuoteContext,
                    priority: 30,
                    bypasses_cache: true,
                    matcher: IntentMatcher::Semantic,
                },
                // ── Syntactic channel intents ─────────────────────
                // These are length-gated short-form patterns that must resolve
                // without an embedding round-trip.
                IntentDescriptor {
                    intent: Intent::Contradiction,
                    priority: 95,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Syntactic(match_contradiction),
                },
                IntentDescriptor {
                    intent: Intent::ShortFollowup,
                    priority: 93,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Syntactic(match_short_followup),
                },
                IntentDescriptor {
                    intent: Intent::ReactiveSarcasm,
                    priority: 91,
                    bypasses_cache: false,
                    matcher: IntentMatcher::Syntactic(match_reactive_sarcasm),
                },
            ],
        }
    }

    /// Classify a prompt synchronously against **syntactic** matchers only.
    ///
    /// Returns at most the three short-form channel intents (Contradiction,
    /// ShortFollowup, ReactiveSarcasm). For full classification including
    /// semantic intents, call `classify_semantic()` instead.
    pub fn classify(&self, prompt: &str) -> Vec<Intent> {
        let lower = prompt.to_ascii_lowercase();
        let mut matches: Vec<(Intent, u8)> = self
            .descriptors
            .iter()
            .filter(|d| {
                if let IntentMatcher::Syntactic(f) = d.matcher {
                    f(&lower)
                } else {
                    false
                }
            })
            .map(|d| (d.intent, d.priority))
            .collect();
        matches.sort_by(|a, b| b.1.cmp(&a.1));
        matches.into_iter().map(|(intent, _)| intent).collect()
    }

    /// Classify a prompt against all intents using semantic + syntactic matching.
    ///
    /// Semantic intents are scored by the [`SemanticClassifier`] at `threshold`
    /// cosine similarity. Syntactic intents are always evaluated. Results are
    /// merged and sorted by priority (highest first).
    ///
    /// Logs `intent.<name>.score` and `intent.<name>.source` for each match.
    pub async fn classify_semantic(
        &self,
        prompt: &str,
        classifier: &SemanticClassifier,
        threshold: f64,
    ) -> Vec<Intent> {
        use roboticus_llm::intent_exemplars::INTENT_EXEMPLARS;

        // Syntactic pass (always runs, no I/O).
        let syntactic = self.classify(prompt);

        // Semantic pass via classifier (multi-label, no abstain).
        let semantic_results = match classifier
            .classify(prompt, INTENT_EXEMPLARS, threshold, None)
            .await
        {
            Ok(results) => results,
            Err(e) => {
                tracing::warn!(error = %e, "semantic intent classification failed; falling back to syntactic only");
                return syntactic;
            }
        };

        // Build a priority map from descriptors.
        let priority_map: std::collections::HashMap<Intent, u8> = self
            .descriptors
            .iter()
            .map(|d| (d.intent, d.priority))
            .collect();

        // Map semantic results → Intent variants and log scores.
        let mut combined: Vec<(Intent, u8)> = Vec::new();
        for result in &semantic_results {
            if let Some(intent) = Intent::from_category(&result.category) {
                let priority = priority_map.get(&intent).copied().unwrap_or(0);
                tracing::debug!(
                    intent = %result.category,
                    score = result.score,
                    trust = ?result.trust,
                    source = "semantic",
                    "intent classification"
                );
                combined.push((intent, priority));
            }
        }

        // Merge syntactic results (channel intents), logging source.
        for intent in &syntactic {
            let priority = priority_map.get(intent).copied().unwrap_or(0);
            tracing::debug!(
                intent = ?intent,
                source = "syntactic",
                "intent classification"
            );
            if !combined.iter().any(|(i, _)| i == intent) {
                combined.push((*intent, priority));
            }
        }

        // Suppress execution intents when the prompt is primarily an affirmative
        // continuation. Words like "awesome", "great", "yes", "sounds good" at the
        // end of a context-heavy prompt should not trigger tool execution — they're
        // the user agreeing with the agent's previous proposal.
        if is_affirmative_continuation(prompt) {
            combined.retain(|(intent, _)| {
                !matches!(
                    intent,
                    Intent::Execution
                        | Intent::FileDistribution
                        | Intent::FolderScan
                        | Intent::WalletAddressScan
                        | Intent::ImageCountScan
                        | Intent::MarkdownCountScan
                        | Intent::ObsidianInsights
                        | Intent::EmailTriage
                )
            });
        }

        combined.sort_by(|a, b| b.1.cmp(&a.1));
        combined.into_iter().map(|(intent, _)| intent).collect()
    }
}

/// Detect prompts that are primarily affirmative continuations — the user
/// agreeing with a previous proposal rather than issuing a new command.
/// Patterns: "awesome", "great, do it", "yes please", "sounds good", or
/// a context sentence followed by a short affirmative ("... files. awesome").
fn is_affirmative_continuation(prompt: &str) -> bool {
    let affirmatives = [
        "awesome",
        "great",
        "perfect",
        "cool",
        "nice",
        "sounds good",
        "sounds great",
        "let's do it",
        "do it",
        "go for it",
        "go ahead",
        "proceed",
        "yes",
        "yes please",
        "yep",
        "yeah",
        "ok",
        "okay",
        "sure",
        "absolutely",
        "agreed",
        "exactly",
        "love it",
    ];

    let lower = prompt.trim().to_ascii_lowercase();

    // Case 1: The entire prompt is just an affirmative word/phrase
    if affirmatives
        .iter()
        .any(|a| lower == *a || lower == format!("{a}.") || lower == format!("{a}!"))
    {
        return true;
    }

    // Case 2: The prompt ends with an affirmative after context
    // e.g., "We agreed to build X. awesome"
    if let Some(last_sentence) = lower.rsplit_terminator(['.', '!', '?']).next() {
        let trimmed = last_sentence.trim();
        if !trimmed.is_empty() && affirmatives.contains(&trimmed) {
            return true;
        }
    }

    // Case 3: Last word is a standalone affirmative
    if let Some(last_word) = lower.split_whitespace().last() {
        let clean = last_word.trim_matches(|c: char| c.is_ascii_punctuation());
        if affirmatives.contains(&clean) && lower.split_whitespace().count() > 2 {
            return true;
        }
    }

    false
}

impl IntentRegistry {
    /// Returns true if any of the matched intents require cache bypass.
    pub fn should_bypass_cache(&self, intents: &[Intent]) -> bool {
        let set: HashSet<Intent> = intents.iter().copied().collect();
        self.descriptors
            .iter()
            .any(|d| d.bypasses_cache && set.contains(&d.intent))
    }
}

// ── Syntactic matcher functions ──────────────────────────────────────────

/// Short contradiction follow-up from `is_short_contradiction_followup()`.
/// Length-gated to ≤48 chars (trimmed).
fn match_contradiction(lower: &str) -> bool {
    let trimmed = lower.trim();
    if trimmed.len() > 48 {
        return false;
    }
    const MARKERS: &[&str] = &[
        "that's not true",
        "that is not true",
        "not true",
        "that's wrong",
        "that is wrong",
        "incorrect",
    ];
    MARKERS.iter().any(|m| trimmed.contains(m))
}

/// Short follow-up referencing a previous reply from
/// `is_short_followup_for_previous_reply()`.
/// Length-gated to ≤80 chars (trimmed).
fn match_short_followup(lower: &str) -> bool {
    let trimmed = lower.trim();
    if trimmed.len() > 80 {
        return false;
    }
    const MARKERS: &[&str] = &[
        "what's that from",
        "what is that from",
        "where is that from",
        "no, your quote",
        "your quote",
        "what quote",
        "source?",
    ];
    MARKERS.iter().any(|m| trimmed.contains(m))
}

/// Short reactive sarcasm from `is_short_reactive_sarcasm()`.
/// Length-gated to ≤32 chars (trimmed). Uses exact/suffix match
/// (marker, marker., marker...) — not substring.
fn match_reactive_sarcasm(lower: &str) -> bool {
    let trimmed = lower.trim();
    if trimmed.len() > 32 {
        return false;
    }
    const MARKERS: &[&str] = &[
        "wow",
        "great",
        "fantastic",
        "amazing",
        "incredible",
        "brilliant",
        "sure",
        "right",
    ];
    MARKERS.iter().any(|m| {
        trimmed == *m || {
            // Allocation-free suffix strip: check for "marker." or "marker..."
            let stripped = trimmed
                .strip_suffix("...")
                .or_else(|| trimmed.strip_suffix('.'));
            stripped.is_some_and(|s| s == *m)
        }
    })
}

// ── Tests ────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use roboticus_llm::embedding::EmbeddingClient;

    /// Build a classifier backed by the n-gram fallback (no remote provider).
    fn make_classifier() -> SemanticClassifier {
        SemanticClassifier::new(EmbeddingClient::new(None).unwrap())
    }

    /// Classify a prompt using semantic + syntactic matching (n-gram fallback).
    ///
    /// Uses a lower threshold (0.50) than the production default (0.60) because
    /// the n-gram fallback embedding produces lower absolute similarity scores
    /// than a real embedding provider — the relative ordering is still correct.
    async fn classify(prompt: &str) -> Vec<Intent> {
        let classifier = make_classifier();
        IntentRegistry::default_registry()
            .classify_semantic(prompt, &classifier, 0.50)
            .await
    }

    async fn has(prompt: &str, intent: Intent) -> bool {
        classify(prompt).await.contains(&intent)
    }

    fn bypass(intents: &[Intent]) -> bool {
        IntentRegistry::default_registry().should_bypass_cache(intents)
    }

    // ── Parity tests ─────────────────────────────────────────────────

    #[tokio::test]
    async fn execution_markers_cover_shortcut_and_guard_triggers() {
        // Use verbatim or near-verbatim exemplar phrases for reliable n-gram scoring.
        // The n-gram fallback produces lower scores than real embeddings, so phrases
        // must be close to exemplars to exceed the 0.50 threshold.
        assert!(has("use a tool to accomplish this task", Intent::Execution).await);
        assert!(has("run the command and show me the output", Intent::Execution).await);
    }

    #[tokio::test]
    async fn delegation_and_cron_markers_match_expected_prompts() {
        assert!(has("assign this work to a subagent", Intent::Delegation).await);
        // Use verbatim exemplar phrase for reliable n-gram scoring.
        assert!(has("schedule a cron job for this", Intent::Cron).await);
    }

    #[tokio::test]
    async fn model_identity_markers_match_expected_prompts() {
        assert!(has("what model are you using right now", Intent::ModelIdentity,).await);
        assert!(has("which AI model is this", Intent::ModelIdentity).await);
    }

    #[tokio::test]
    async fn current_events_markers_match_expected_prompts() {
        assert!(
            has(
                "give me a geopolitical update on world events",
                Intent::CurrentEvents,
            )
            .await
        );
        assert!(has("what's happening in the world today", Intent::CurrentEvents,).await);
    }

    #[tokio::test]
    async fn introspection_markers_match_expected_prompts() {
        assert!(has("what tools do you have available", Intent::Introspection,).await);
        assert!(
            has(
                "list the tools you can currently use",
                Intent::Introspection,
            )
            .await
        );
    }

    #[tokio::test]
    async fn email_triage_markers_match_expected_prompts() {
        assert!(has("check my email inbox", Intent::EmailTriage,).await);
        assert!(has("triage my unread messages", Intent::EmailTriage,).await);
    }

    #[tokio::test]
    async fn literary_quote_markers_match_expected_prompts() {
        assert!(
            has(
                "give me a Dune quote for this conflict",
                Intent::LiteraryQuoteContext,
            )
            .await
        );
    }

    #[tokio::test]
    async fn acknowledgement_markers_match_expected_prompts() {
        assert!(
            has(
                "acknowledge this and wait for my next command",
                Intent::Acknowledgement,
            )
            .await
        );
    }

    #[tokio::test]
    async fn provider_inventory_markers_match_expected_prompts() {
        assert!(has("which LLM providers do you have", Intent::ProviderInventory).await);
        assert!(
            has(
                "what AI providers are configured",
                Intent::ProviderInventory,
            )
            .await
        );
    }

    #[tokio::test]
    async fn personality_and_capability_markers_match_expected_prompts() {
        assert!(has("tell me about your personality", Intent::PersonalityProfile,).await);
        assert!(has("who are you", Intent::PersonalityProfile).await);
        assert!(has("what are you able to do", Intent::CapabilitySummary,).await);
    }

    #[tokio::test]
    async fn wallet_scan_markers_match_expected_prompts() {
        assert!(
            has(
                "find wallet addresses in my files",
                Intent::WalletAddressScan,
            )
            .await
        );
    }

    #[tokio::test]
    async fn image_count_markers_match_expected_prompts() {
        assert!(has("how many images are in this folder", Intent::ImageCountScan,).await);
        assert!(has("count the photos", Intent::ImageCountScan,).await);
    }

    #[tokio::test]
    async fn markdown_count_markers_match_expected_prompts() {
        assert!(
            has(
                "how many markdown files do I have",
                Intent::MarkdownCountScan,
            )
            .await
        );
        // Use exemplar-adjacent phrase for reliable n-gram scoring.
        assert!(
            has(
                "count all .md and .markdown files",
                Intent::MarkdownCountScan,
            )
            .await
        );
    }

    #[tokio::test]
    async fn folder_scan_markers_match_expected_prompts() {
        assert!(has("look in my downloads folder", Intent::FolderScan,).await);
        assert!(has("scan the documents directory", Intent::FolderScan,).await);
    }

    #[tokio::test]
    async fn obsidian_insight_markers_match_expected_prompts() {
        assert!(has("summarize my Obsidian vault", Intent::ObsidianInsights,).await);
    }

    // ── Cache bypass parity ──────────────────────────────────────────

    #[tokio::test]
    async fn cache_bypass_markers_cover_shortcut_handled_prompts() {
        let intents = classify("use a tool to accomplish this task").await;
        assert!(bypass(&intents));

        let intents2 = classify("what's happening in the world today").await;
        assert!(bypass(&intents2));

        // Empty intents slice must NOT bypass cache (deterministic check).
        assert!(!bypass(&[]));
    }

    // ── Priority ordering ────────────────────────────────────────────

    #[test]
    fn acknowledgement_has_higher_priority_than_current_events_in_descriptor() {
        let reg = IntentRegistry::default_registry();
        let ack = reg
            .descriptors
            .iter()
            .find(|d| d.intent == Intent::Acknowledgement)
            .unwrap()
            .priority;
        let ce = reg
            .descriptors
            .iter()
            .find(|d| d.intent == Intent::CurrentEvents)
            .unwrap()
            .priority;
        assert!(
            ack > ce,
            "Acknowledgement (priority {ack}) must exceed CurrentEvents (priority {ce})"
        );
    }

    #[test]
    fn model_identity_higher_priority_than_execution() {
        let reg = IntentRegistry::default_registry();
        let mi = reg
            .descriptors
            .iter()
            .find(|d| d.intent == Intent::ModelIdentity)
            .unwrap()
            .priority;
        let ex = reg
            .descriptors
            .iter()
            .find(|d| d.intent == Intent::Execution)
            .unwrap()
            .priority;
        assert!(
            mi > ex,
            "ModelIdentity (priority {mi}) should exceed Execution (priority {ex})"
        );
    }

    // ── Channel intent parity ────────────────────────────────────────

    #[test]
    fn contradiction_matches_short_prompts() {
        let reg = IntentRegistry::default_registry();
        assert!(
            reg.classify("that's not true")
                .contains(&Intent::Contradiction)
        );
        assert!(
            reg.classify("That is wrong.")
                .contains(&Intent::Contradiction)
        );
        assert!(reg.classify("incorrect").contains(&Intent::Contradiction));
        // Over 48 chars → rejected
        assert!(
            !reg.classify("I think that is not true based on extensive research and evidence")
                .contains(&Intent::Contradiction)
        );
    }

    #[test]
    fn short_followup_matches_quote_references() {
        let reg = IntentRegistry::default_registry();
        assert!(
            reg.classify("what's that from?")
                .contains(&Intent::ShortFollowup)
        );
        assert!(
            reg.classify("Where is that from?")
                .contains(&Intent::ShortFollowup)
        );
        assert!(reg.classify("source?").contains(&Intent::ShortFollowup));
        // Over 80 chars → rejected
        let long = format!(
            "What's that from? I need to know because {}",
            "a".repeat(80)
        );
        assert!(!reg.classify(&long).contains(&Intent::ShortFollowup));
    }

    #[test]
    fn reactive_sarcasm_matches_exact_and_suffixed() {
        let reg = IntentRegistry::default_registry();
        assert!(reg.classify("wow").contains(&Intent::ReactiveSarcasm));
        assert!(reg.classify("Great.").contains(&Intent::ReactiveSarcasm));
        assert!(
            reg.classify("fantastic...")
                .contains(&Intent::ReactiveSarcasm)
        );
        assert!(reg.classify("  sure  ").contains(&Intent::ReactiveSarcasm));
        // Not a substring match — "wow that was great" should NOT match
        assert!(
            !reg.classify("wow that was great")
                .contains(&Intent::ReactiveSarcasm)
        );
        // Over 32 chars → rejected
        assert!(
            !reg.classify("wow this is incredibly amazing work")
                .contains(&Intent::ReactiveSarcasm)
        );
    }

    #[test]
    fn channel_intents_have_highest_priorities() {
        // Contradiction (95) > ShortFollowup (93) > ReactiveSarcasm (91) > all others
        let reg = IntentRegistry::default_registry();
        let channel_priorities: Vec<u8> = reg
            .descriptors
            .iter()
            .filter(|d| {
                matches!(
                    d.intent,
                    Intent::Contradiction | Intent::ShortFollowup | Intent::ReactiveSarcasm
                )
            })
            .map(|d| d.priority)
            .collect();
        let max_standard: u8 = reg
            .descriptors
            .iter()
            .filter(|d| {
                !matches!(
                    d.intent,
                    Intent::Contradiction | Intent::ShortFollowup | Intent::ReactiveSarcasm
                )
            })
            .map(|d| d.priority)
            .max()
            .unwrap_or(0);
        assert!(
            channel_priorities.iter().all(|&p| p > max_standard),
            "Channel intents must have higher priority than all standard intents"
        );
    }

    // ── Case insensitivity ───────────────────────────────────────────

    #[test]
    fn classify_lowercases_once_for_syntactic_intents() {
        let reg = IntentRegistry::default_registry();
        // Syntactic matchers must be case-insensitive
        assert!(reg.classify("INCORRECT").contains(&Intent::Contradiction));
        assert!(reg.classify("WOW").contains(&Intent::ReactiveSarcasm));
    }

    // ── Negative / false-positive prevention tests ───────────────────

    #[tokio::test]
    async fn bashar_does_not_match_execution() {
        assert!(!has("bashar", Intent::Execution).await);
    }

    #[tokio::test]
    async fn what_is_delegation_does_not_match_delegation() {
        // A question about delegation is not a delegation command.
        assert!(!has("what is delegation", Intent::Delegation).await);
    }

    #[tokio::test]
    async fn i_would_like_to_play_a_game_does_not_match_execution() {
        assert!(!has("I would like to play a game", Intent::Execution).await);
    }
}
