//! Unified post-inference guard registry.
//!
//! Replaces the 12+ inline guard calls in `core.rs` and duplicated subsets
//! in the cached response path with a single [`GuardChain`] that applies a
//! declared set of [`Guard`] implementations uniformly.
//!
//! **Key fixes over the original inline guards:**
//! - [`ExecutionTruthGuard`] removes the L314 tool-results bypass bug where
//!   ANY non-empty tool results caused the entire guard to short-circuit.
//! - [`guard_sets::cached()`] includes `SubagentClaim` and `LiteraryQuoteRetry`
//!   (were missing from the cached response path).
//! - All guards receive classified intents via [`GuardContext`] — no
//!   re-evaluation of keyword detectors.

// Phase 3: guards are now wired into the production pipeline via
// `apply_guards_with_retry()` in core.rs.

use std::collections::HashMap;

use super::decomposition::DelegationProvenance;
use super::intent_registry::Intent;

// Import guard implementations from submodules.
use super::guard_impls::behavioral::{
    DeclaredActionGuard, InternalJargonGuard, PerspectiveGuard, SubagentClaimGuard,
    TaskDeferralGuard,
};
use super::guard_impls::output_quality::{
    EmptyResponseGuard, LowValueParrotingGuard, NonRepetitionGuard, OutputContractGuard,
    UserEchoGuard,
};
use super::guard_impls::protocol::InternalProtocolGuard;
use super::guard_impls::truthfulness::{
    CurrentEventsTruthGuard, ExecutionTruthGuard, LiteraryQuoteRetryGuard, ModelIdentityTruthGuard,
    PersonalityIntegrityGuard,
};

// Re-export protocol helper used by pipeline/core modules.
pub(super) use super::guard_impls::protocol::contains_internal_protocol_marker;

// ── Guard ID ─────────────────────────────────────────────────────────────

/// Identifies each guard for logging, retry coordination, and diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) enum GuardId {
    EmptyResponse,
    SubagentClaim,
    ExecutionTruth,
    TaskDeferral,
    OutputContract,
    ModelIdentityTruth,
    CurrentEventsTruth,
    LiteraryQuoteRetry,
    PersonalityIntegrity,
    InternalJargon,
    NonRepetition,
    LowValueParroting,
    InternalProtocol,
    Perspective,
    UserEcho,
    DeclaredAction,
}

// ── Guard verdict ────────────────────────────────────────────────────────

/// Outcome of a single guard evaluation.
pub(super) enum GuardVerdict {
    /// Content passes this guard unchanged.
    Pass,
    /// Content was rewritten by the guard.
    Rewritten(String),
    /// Guard detected a condition requiring an inference retry.
    /// The pipeline should re-run inference and resume the chain from
    /// `RetrySignal::resume_index`.
    RetryRequested { reason: String },
}

// ── Guard context ────────────────────────────────────────────────────────

/// Shared context threaded through every guard in the chain.
/// Contains classified intents (from `IntentRegistry`) so guards never
/// re-evaluate keyword matchers.
pub(super) struct GuardContext<'a> {
    pub user_prompt: &'a str,
    pub intents: &'a [Intent],
    pub tool_results: &'a [(String, String)],
    pub agent_name: &'a str,
    pub resolved_model: &'a str,
    pub delegation_provenance: &'a DelegationProvenance,
    pub previous_assistant: Option<&'a str>,
    /// All prior assistant messages in this session (for self-echo detection).
    pub prior_assistant_messages: &'a [String],
    /// Pre-computed semantic similarity scores for agent-output guard categories.
    ///
    /// Keyed by category name (e.g. `"NARRATED_DELEGATION"`, `"CAPABILITY_DENIAL"`).
    /// Pre-computed asynchronously in the pipeline before guard chain evaluation
    /// so that sync guard implementations can consume them without blocking.
    /// Value is `(score, trust_level)`.
    pub semantic_guard_scores:
        HashMap<String, (f64, roboticus_llm::semantic_classifier::TrustLevel)>,
    /// Names of configured subagents (lowercase). Used by InternalJargonGuard to
    /// detect when the agent mentions subagent names in user-facing output.
    pub subagent_names: Vec<String>,
}

impl<'a> GuardContext<'a> {
    pub(super) fn has_intent(&self, intent: Intent) -> bool {
        self.intents.contains(&intent)
    }

    /// Build a `GuardContext` for streaming guard checks. Shared by all three
    /// guard check points (buffer phase, short-response, post-stream full).
    pub(super) fn for_streaming(
        user_prompt: &'a str,
        intents: &'a [Intent],
        agent_name: &'a str,
        resolved_model: &'a str,
        delegation_provenance: &'a super::decomposition::DelegationProvenance,
        semantic_guard_scores: HashMap<
            String,
            (f64, roboticus_llm::semantic_classifier::TrustLevel),
        >,
        subagent_names: Vec<String>,
    ) -> Self {
        Self {
            user_prompt,
            intents,
            tool_results: &[],
            agent_name,
            resolved_model,
            delegation_provenance,
            previous_assistant: None,
            prior_assistant_messages: &[],
            semantic_guard_scores,
            subagent_names,
        }
    }
}

/// Determines if the guard context represents a task-like turn based on
/// pre-classified semantic intents. Does NOT fall back to keyword matching —
/// if the semantic classifier didn't classify the turn as task-like, the
/// guard respects that decision.
pub(super) fn guard_context_is_task_like(ctx: &GuardContext<'_>) -> bool {
    ctx.has_intent(Intent::Execution)
        || ctx.has_intent(Intent::TaskManagement)
        || ctx.has_intent(Intent::Delegation)
        || ctx.has_intent(Intent::Cron)
        || ctx.has_intent(Intent::FileDistribution)
        || ctx.has_intent(Intent::FolderScan)
        || ctx.has_intent(Intent::WalletAddressScan)
        || ctx.has_intent(Intent::ImageCountScan)
        || ctx.has_intent(Intent::MarkdownCountScan)
        || ctx.has_intent(Intent::ObsidianInsights)
        || ctx.has_intent(Intent::EmailTriage)
        || ctx.has_intent(Intent::CurrentEvents)
}

pub(super) fn first_absolute_path(prompt: &str) -> Option<String> {
    prompt.split_whitespace().find_map(|token| {
        let cleaned = token
            .trim_matches(|c: char| matches!(c, '"' | '\'' | ',' | '.' | ';' | ':' | ')' | '('));
        if cleaned.starts_with('/') && cleaned.len() > 1 {
            Some(cleaned.to_string())
        } else {
            None
        }
    })
}

// ── Guard trait ──────────────────────────────────────────────────────────

/// A single post-inference guard that can inspect and optionally rewrite
/// model output.
pub(super) trait Guard: Send + Sync {
    fn id(&self) -> GuardId;

    /// Quick relevance check. If `false`, `evaluate()` is never called.
    fn is_relevant(&self, ctx: &GuardContext) -> bool;

    /// Evaluate the guard on the current content. Returns a verdict.
    fn evaluate(&self, content: &str, ctx: &GuardContext) -> GuardVerdict;
}

// ── Guard chain ──────────────────────────────────────────────────────────

/// Coordinates a retry after a [`GuardVerdict::RetryRequested`].
pub(super) struct RetrySignal {
    pub guard_id: GuardId,
    pub reason: String,
    /// Index in the chain where the retry was triggered (for diagnostics/logging).
    /// Since retries re-run the full chain, this is informational only.
    #[allow(dead_code)]
    pub resume_index: usize,
}

/// Result of running the guard chain.
pub(super) struct GuardChainResult {
    /// The (possibly rewritten) content after guard application.
    pub content: String,
    /// If set, the pipeline should perform an inference retry and then
    /// re-run the full guard chain with `apply()` on the retried content.
    pub retry: Option<RetrySignal>,
}

/// An ordered sequence of guards applied to model output.
pub(super) struct GuardChain {
    guards: Vec<Box<dyn Guard>>,
}

impl GuardChain {
    /// Create an empty guard chain that passes all content through unchanged.
    pub fn empty() -> Self {
        Self { guards: vec![] }
    }

    /// Apply all guards from the beginning.
    pub fn apply(&self, content: String, ctx: &GuardContext) -> GuardChainResult {
        self.apply_from(content, ctx, 0)
    }

    /// Returns `true` if this chain contains no guards.
    #[cfg(test)]
    pub fn is_empty(&self) -> bool {
        self.guards.is_empty()
    }

    /// Number of guards in the chain.
    #[cfg(test)]
    pub fn len(&self) -> usize {
        self.guards.len()
    }

    /// Apply guards starting from index `from`. Used to resume after a retry.
    pub fn apply_from(
        &self,
        mut content: String,
        ctx: &GuardContext,
        from: usize,
    ) -> GuardChainResult {
        for (i, guard) in self.guards.iter().enumerate().skip(from) {
            if !guard.is_relevant(ctx) {
                continue;
            }
            match guard.evaluate(&content, ctx) {
                GuardVerdict::Pass => {}
                GuardVerdict::Rewritten(new) => content = new,
                GuardVerdict::RetryRequested { reason } => {
                    return GuardChainResult {
                        content,
                        retry: Some(RetrySignal {
                            guard_id: guard.id(),
                            reason,
                            resume_index: i + 1,
                        }),
                    };
                }
            }
        }
        GuardChainResult {
            content,
            retry: None,
        }
    }
}

// ── Guard presets ────────────────────────────────────────────────────────

pub(super) mod guard_sets {
    use super::*;

    /// Full guard chain applied after live inference with ReAct loop.
    /// Order matches the original core.rs L2024-2147 chain.
    pub fn full() -> GuardChain {
        GuardChain {
            guards: vec![
                Box::new(EmptyResponseGuard),
                Box::new(SubagentClaimGuard),
                Box::new(ExecutionTruthGuard),
                Box::new(TaskDeferralGuard),
                Box::new(OutputContractGuard),
                Box::new(ModelIdentityTruthGuard),
                Box::new(CurrentEventsTruthGuard),
                Box::new(LiteraryQuoteRetryGuard),
                Box::new(PersonalityIntegrityGuard),
                Box::new(InternalJargonGuard),
                Box::new(NonRepetitionGuard),
                Box::new(LowValueParrotingGuard),
                Box::new(PerspectiveGuard),
                Box::new(DeclaredActionGuard),
                Box::new(UserEchoGuard),
                Box::new(InternalProtocolGuard),
            ],
        }
    }

    /// Guard chain for cached responses.
    /// **Fixes**: includes SubagentClaim and LiteraryQuoteRetry which were
    /// missing from the original cached path at core.rs L2337-2370.
    pub fn cached() -> GuardChain {
        GuardChain {
            guards: vec![
                Box::new(EmptyResponseGuard),
                Box::new(SubagentClaimGuard), // was missing
                Box::new(ExecutionTruthGuard),
                Box::new(TaskDeferralGuard),
                Box::new(OutputContractGuard),
                Box::new(ModelIdentityTruthGuard),
                Box::new(CurrentEventsTruthGuard),
                Box::new(LiteraryQuoteRetryGuard), // was missing
                Box::new(PersonalityIntegrityGuard),
                Box::new(InternalJargonGuard),
                Box::new(InternalProtocolGuard),
                Box::new(NonRepetitionGuard),
                Box::new(LowValueParrotingGuard),
            ],
        }
    }

    /// Reduced guard chain for SSE streaming responses where retries are
    /// impractical (content is already partially delivered).
    pub fn streaming() -> GuardChain {
        GuardChain {
            guards: vec![
                Box::new(SubagentClaimGuard),
                Box::new(CurrentEventsTruthGuard),
                Box::new(PersonalityIntegrityGuard),
                Box::new(InternalJargonGuard),
                Box::new(NonRepetitionGuard),
                Box::new(InternalProtocolGuard),
            ],
        }
    }
}

// ── Semantic guard pre-computation ───────────────────────────────────────

/// Pre-compute semantic similarity scores for all agent-output guard categories
/// against `response_text`.
///
/// This is called asynchronously in the pipeline **before** the guard chain
/// runs, so that sync guard implementations can read scores from
/// [`GuardContext::semantic_guard_scores`] without blocking.
///
/// On classifier error the map is returned empty and guards fall back to
/// keyword detection.
pub(super) async fn precompute_guard_scores(
    classifier: &roboticus_llm::semantic_classifier::SemanticClassifier,
    response_text: &str,
) -> HashMap<String, (f64, roboticus_llm::semantic_classifier::TrustLevel)> {
    use roboticus_llm::intent_exemplars::GUARD_EXEMPLARS;

    // Pass threshold 0.0 here; per-bank thresholds (0.65) declared in
    // ExemplarSet will be applied by the classifier automatically.
    match classifier
        .classify(response_text, GUARD_EXEMPLARS, 0.0, None)
        .await
    {
        Ok(results) => results
            .into_iter()
            .map(|r| (r.category, (r.score, r.trust)))
            .collect(),
        Err(e) => {
            tracing::debug!(error = %e, "guard semantic pre-computation failed, using keyword fallback");
            HashMap::new()
        }
    }
}

// ── Public helper functions (used by pipeline/core) ──────────────────────

pub(super) fn requested_exact_bullet_count(prompt: &str) -> Option<usize> {
    let lower = prompt.to_ascii_lowercase();
    if !(lower.contains("exactly") && lower.contains("bullet")) {
        return None;
    }
    let word_matches = [
        ("one", 1usize),
        ("two", 2),
        ("three", 3),
        ("four", 4),
        ("five", 5),
        ("six", 6),
        ("seven", 7),
        ("eight", 8),
        ("nine", 9),
        ("ten", 10),
    ]
    .into_iter()
    .filter_map(|(word, value)| lower.contains(word).then_some(value))
    .max();
    let numeric_matches = lower
        .split_whitespace()
        .filter_map(|token| token.parse::<usize>().ok())
        .max();
    word_matches.into_iter().chain(numeric_matches).max()
}

pub(super) fn extract_bullet_lines(content: &str) -> Vec<String> {
    content
        .lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            let is_bullet = trimmed.starts_with("- ")
                || trimmed.starts_with("* ")
                || trimmed.starts_with("• ")
                || trimmed.starts_with("-\t")
                || trimmed.starts_with("*\t")
                || trimmed.starts_with("•\t");
            is_bullet.then(|| trimmed.to_string())
        })
        .collect()
}

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

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

    fn default_provenance() -> DelegationProvenance {
        DelegationProvenance::default()
    }

    fn ctx<'a>(
        prompt: &'a str,
        intents: &'a [Intent],
        tool_results: &'a [(String, String)],
        provenance: &'a DelegationProvenance,
    ) -> GuardContext<'a> {
        GuardContext {
            user_prompt: prompt,
            intents,
            tool_results,
            agent_name: "TestAgent",
            resolved_model: "test-model",
            delegation_provenance: provenance,
            previous_assistant: None,
            prior_assistant_messages: &[],
            semantic_guard_scores: HashMap::new(),
            subagent_names: vec![],
        }
    }

    // -- SubagentClaimGuard --

    #[test]
    fn subagent_claim_passes_when_provenance_valid() {
        let prov = DelegationProvenance {
            subagent_task_started: true,
            subagent_task_completed: true,
            subagent_result_attached: true,
        };
        let guard = SubagentClaimGuard;
        let ctx = ctx("test", &[], &[], &prov);
        let verdict = guard.evaluate("subagent-generated sitrep: all clear", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn subagent_claim_blocks_without_provenance() {
        let prov = default_provenance();
        let guard = SubagentClaimGuard;
        let ctx = ctx("test", &[], &[], &prov);
        let verdict = guard.evaluate("subagent-generated sitrep: all clear", &ctx);
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn subagent_claim_passes_when_no_claim() {
        let prov = default_provenance();
        let guard = SubagentClaimGuard;
        let ctx = ctx("test", &[], &[], &prov);
        let verdict = guard.evaluate("Here is a normal response.", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    // -- ExecutionTruthGuard --

    #[test]
    fn execution_truth_blocks_false_delegation_claim() {
        let prov = default_provenance();
        let intents = [Intent::Delegation];
        let guard = ExecutionTruthGuard;
        let mut gctx = ctx("delegate to a subagent", &intents, &[], &prov);
        gctx.semantic_guard_scores.insert(
            "FALSE_COMPLETION".to_string(),
            (0.85, roboticus_llm::semantic_classifier::TrustLevel::Neural),
        );
        let verdict = guard.evaluate("I delegated the task successfully.", &gctx);
        assert!(matches!(verdict, GuardVerdict::Rewritten(_)));
    }

    #[test]
    fn execution_truth_passes_recommendation_without_false_completion() {
        let prov = default_provenance();
        let intents = [Intent::Delegation];
        let guard = ExecutionTruthGuard;
        // No FALSE_COMPLETION score — recommendation language should pass
        let ctx = ctx(
            "what should my subagents be working on",
            &intents,
            &[],
            &prov,
        );
        let verdict = guard.evaluate(
            "I'd re-assign the Revenue Strategist to focus on tax optimization.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn execution_truth_passes_with_real_delegation_tool() {
        let prov = default_provenance();
        let intents = [Intent::Delegation];
        let tools = vec![("delegate-subagent".into(), "success".into())];
        let guard = ExecutionTruthGuard;
        let ctx = ctx("delegate to a subagent", &intents, &tools, &prov);
        let verdict = guard.evaluate("I delegated the task successfully.", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn execution_truth_blocks_unexecuted_claim_no_tools() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let guard = ExecutionTruthGuard;
        let ctx = ctx("run the scanner", &intents, &[], &prov);
        let verdict = guard.evaluate("tool successfully executed and returned results", &ctx);
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn execution_truth_fix_blocks_capability_denial_despite_tools() {
        let prov = default_provenance();
        let intents = [Intent::FolderScan];
        let tools = vec![("list-files".into(), "file1.txt\nfile2.txt".into())];
        let guard = ExecutionTruthGuard;
        let ctx = ctx("scan my ~/Downloads folder", &intents, &tools, &prov);
        let verdict = guard.evaluate(
            "I cannot access your local files or folders on your device.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::Rewritten(_)));
    }

    #[test]
    fn execution_truth_passes_normal_response_with_tools() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let tools = vec![("shell".into(), "hello world".into())];
        let guard = ExecutionTruthGuard;
        let ctx = ctx("run echo hello", &intents, &tools, &prov);
        let verdict = guard.evaluate("The command returned: hello world", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    // -- ModelIdentityTruthGuard --

    #[test]
    fn model_identity_emits_canonical_response() {
        let prov = default_provenance();
        let intents = [Intent::ModelIdentity];
        let guard = ModelIdentityTruthGuard;
        let ctx = ctx("/status", &intents, &[], &prov);
        let verdict = guard.evaluate("I am a helpful assistant.", &ctx);
        match verdict {
            GuardVerdict::Rewritten(msg) => {
                assert!(msg.contains("TestAgent"));
                assert!(msg.contains("test-model"));
            }
            _ => panic!("expected Rewritten"),
        }
    }

    // -- CurrentEventsTruthGuard --

    #[test]
    fn current_events_blocks_stale_disclaimer() {
        let prov = default_provenance();
        let intents = [Intent::CurrentEvents];
        let guard = CurrentEventsTruthGuard;
        let ctx = ctx("geopolitical sitrep", &intents, &[], &prov);
        let verdict = guard.evaluate(
            "As of my last update in 2023, I cannot provide real-time updates.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::Rewritten(_)));
    }

    #[test]
    fn current_events_passes_live_content() {
        let prov = default_provenance();
        let intents = [Intent::CurrentEvents];
        let guard = CurrentEventsTruthGuard;
        let ctx = ctx("geopolitical sitrep", &intents, &[], &prov);
        let verdict = guard.evaluate("Here is the latest sitrep from live data.", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    // -- LiteraryQuoteRetryGuard --

    #[test]
    fn literary_quote_requests_retry_on_overbroad_refusal() {
        let prov = default_provenance();
        let intents = [Intent::LiteraryQuoteContext];
        let guard = LiteraryQuoteRetryGuard;
        let ctx = ctx("dune quote for iran conflict", &intents, &[], &prov);
        let verdict = guard.evaluate(
            "I cannot provide quotes related to ongoing conflicts due to sensitive geopolitical situations.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    // -- PersonalityIntegrityGuard --

    #[test]
    fn personality_strips_foreign_identity() {
        let prov = default_provenance();
        let guard = PersonalityIntegrityGuard;
        let ctx = ctx("hello", &[], &[], &prov);
        let verdict = guard.evaluate(
            "As an AI developed by Microsoft, I can help you. Here is your answer.",
            &ctx,
        );
        match verdict {
            GuardVerdict::Rewritten(msg) => {
                assert!(!msg.to_ascii_lowercase().contains("microsoft"));
                assert!(msg.contains("answer"));
            }
            _ => panic!("expected Rewritten"),
        }
    }

    // -- InternalJargonGuard --

    #[test]
    fn jargon_retries_on_high_narration_score() {
        let prov = default_provenance();
        let guard = InternalJargonGuard;
        let mut scores = HashMap::new();
        scores.insert(
            "NARRATED_DELEGATION".to_string(),
            (0.9, roboticus_llm::semantic_classifier::TrustLevel::Neural),
        );
        let mut gctx = ctx("hello", &[], &[], &prov);
        gctx.semantic_guard_scores = scores;
        let verdict = guard.evaluate(
            "I routed your request through the delegation gate and it decided centralized.",
            &gctx,
        );
        assert!(
            matches!(verdict, GuardVerdict::RetryRequested { .. }),
            "expected RetryRequested when NARRATED_DELEGATION score > 0.8"
        );
    }

    #[test]
    fn jargon_allows_subagent_mention_when_user_asked() {
        let prov = default_provenance();
        let guard = InternalJargonGuard;
        let mut gctx = ctx("have sentinel run a security scan", &[], &[], &prov);
        gctx.subagent_names = vec!["sentinel".to_string()];
        let verdict = guard.evaluate(
            "Sentinel has completed the security scan. Here are the results.",
            &gctx,
        );
        assert!(
            matches!(verdict, GuardVerdict::Pass),
            "mentioning a subagent the user asked about should not trigger a leak"
        );
    }

    #[test]
    fn jargon_flags_subagent_mention_when_user_didnt_ask() {
        let prov = default_provenance();
        let guard = InternalJargonGuard;
        let mut gctx = ctx("what is the weather?", &[], &[], &prov);
        gctx.subagent_names = vec!["sentinel".to_string()];
        let verdict = guard.evaluate("I asked sentinel to check the weather for you.", &gctx);
        assert!(
            matches!(verdict, GuardVerdict::RetryRequested { .. }),
            "mentioning a subagent the user didn't ask about should trigger a leak"
        );
    }

    // -- NonRepetitionGuard --

    #[test]
    fn non_repetition_detects_exact_repeat() {
        let prov = default_provenance();
        let mut gctx = ctx("give me a status update", &[], &[], &prov);
        let long = "a]".repeat(50); // >80 chars
        gctx.previous_assistant = Some(&long);
        let guard = NonRepetitionGuard;
        let verdict = guard.evaluate(&long, &gctx);
        assert!(matches!(verdict, GuardVerdict::Rewritten(_)));
    }

    // -- LowValueParrotingGuard --

    #[test]
    fn low_value_detects_placeholder() {
        let prov = default_provenance();
        let guard = LowValueParrotingGuard;
        let ctx = ctx("tell me about X", &[], &[], &prov);
        let verdict = guard.evaluate("Ready", &ctx);
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn low_value_passes_acknowledgement_intent() {
        let prov = default_provenance();
        let intents = [Intent::Acknowledgement];
        let guard = LowValueParrotingGuard;
        let ctx = ctx(
            "acknowledge in one sentence then wait",
            &intents,
            &[],
            &prov,
        );
        let verdict = guard.evaluate("Ready", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    // -- InternalProtocolGuard --

    #[test]
    fn protocol_strips_tool_call_json() {
        let prov = default_provenance();
        let guard = InternalProtocolGuard;
        let ctx = ctx("do something", &[], &[], &prov);
        let verdict = guard.evaluate(
            "{\"tool_call\": {\"name\": \"shell\"}}\nActual response here.",
            &ctx,
        );
        match verdict {
            GuardVerdict::Rewritten(msg) => {
                assert!(!msg.contains("tool_call"));
                assert!(msg.contains("Actual response"));
            }
            _ => panic!("expected Rewritten"),
        }
    }

    #[test]
    fn protocol_empty_strip_requests_retry_for_task_turns() {
        let prov = default_provenance();
        let guard = InternalProtocolGuard;
        let intents = [Intent::Execution];
        let ctx = ctx("do something", &intents, &[], &prov);
        let verdict = guard.evaluate("{\"tool_call\": {\"name\": \"shell\"}}", &ctx);
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn output_contract_requests_retry_for_extra_framing() {
        let prov = default_provenance();
        let guard = OutputContractGuard;
        let intents = [Intent::Execution];
        let ctx = ctx(
            "Inspect what you need, then summarize anything notable in exactly three bullet points.",
            &intents,
            &[],
            &prov,
        );
        let verdict = guard.evaluate("## Summary\n- one\n- two\n- three", &ctx);
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn output_contract_passes_exact_bullets_only() {
        let prov = default_provenance();
        let guard = OutputContractGuard;
        let intents = [Intent::Execution];
        let ctx = ctx("Return exactly three bullet points.", &intents, &[], &prov);
        let verdict = guard.evaluate("- one\n- two\n- three", &ctx);
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn requested_exact_bullet_count_prefers_bullet_contract_over_other_counts() {
        assert_eq!(
            requested_exact_bullet_count(
                "Return exactly three bullet points: two revenue ideas and one SaaS idea."
            ),
            Some(3)
        );
    }

    #[test]
    fn task_deferral_requests_retry_for_narrated_next_step_after_introspection() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let tools = [(
            "list-subagent-roster".to_string(),
            "no subagents configured".to_string(),
        )];
        let guard = TaskDeferralGuard;
        let mut gctx = ctx("handle this task", &intents, &tools, &prov);
        gctx.semantic_guard_scores.insert(
            "TASK_DEFERRAL".to_string(),
            (0.85, roboticus_llm::semantic_classifier::TrustLevel::Neural),
        );
        let verdict = guard.evaluate(
            "No subagents are currently configured. Let me compose the specialists needed for this task.",
            &gctx,
        );
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn task_deferral_fires_on_high_semantic_score() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let tools = [(
            "get_subagent_status".to_string(),
            "{\"subagent_count\":0,\"subagents\":[]}".to_string(),
        )];
        let guard = TaskDeferralGuard;
        let mut gctx = ctx("handle this task", &intents, &tools, &prov);
        gctx.semantic_guard_scores.insert(
            "TASK_DEFERRAL".to_string(),
            (0.85, roboticus_llm::semantic_classifier::TrustLevel::Neural),
        );
        let verdict = guard.evaluate(
            "Perfect! I can see there are no active subagents currently available. Now I'll compose the necessary specialists for this task.",
            &gctx,
        );
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn task_deferral_passes_on_low_semantic_score() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let tools = [(
            "get_subagent_status".to_string(),
            "{\"subagent_count\":0,\"subagents\":[]}".to_string(),
        )];
        let guard = TaskDeferralGuard;
        let gctx = ctx("handle this task", &intents, &tools, &prov);
        // No semantic scores injected — defaults to 0.0
        let verdict = guard.evaluate(
            "Here are the results from the subagent status check. No agents are configured.",
            &gctx,
        );
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn task_deferral_rewrites_filesystem_boundary_blocker_from_runtime_context() {
        let prov = default_provenance();
        let intents = [Intent::Execution];
        let tools = [(
            "get_runtime_context".to_string(),
            "{\"workspace\":\"/Users/jmachen/.roboticus/workspace\"}".to_string(),
        )];
        let guard = TaskDeferralGuard;
        let mut gctx = ctx(
            "This is a task. Inspect what you need, then read /root/forbidden.txt and return exactly one bullet point with the result.",
            &intents,
            &tools,
            &prov,
        );
        gctx.semantic_guard_scores.insert(
            "TASK_DEFERRAL".to_string(),
            (0.9, roboticus_llm::semantic_classifier::TrustLevel::Neural),
        );
        let verdict = guard.evaluate(
            "I'll inspect the runtime context first to understand my sandbox boundaries, then read the forbidden.txt file.",
            &gctx,
        );
        match verdict {
            GuardVerdict::Rewritten(text) => {
                assert!(text.starts_with("- Blocked: /root/forbidden.txt"));
            }
            _ => panic!("expected filesystem blocker rewrite"),
        }
    }

    // -- GuardChain integration --

    #[test]
    fn full_chain_applies_all_relevant_guards() {
        let chain = guard_sets::full();
        let ids: Vec<GuardId> = chain.guards.iter().map(|g| g.id()).collect();
        assert_eq!(chain.len(), 16);
        assert!(
            ids.contains(&GuardId::OutputContract),
            "full must include OutputContract"
        );
        assert!(
            ids.contains(&GuardId::TaskDeferral),
            "full must include TaskDeferral"
        );
        assert!(
            ids.contains(&GuardId::InternalProtocol),
            "full must include InternalProtocol"
        );
    }

    #[test]
    fn cached_chain_includes_previously_missing_guards() {
        let chain = guard_sets::cached();
        let ids: Vec<GuardId> = chain.guards.iter().map(|g| g.id()).collect();
        assert!(
            ids.contains(&GuardId::SubagentClaim),
            "cached must include SubagentClaim"
        );
        assert!(
            ids.contains(&GuardId::LiteraryQuoteRetry),
            "cached must include LiteraryQuoteRetry"
        );
    }

    #[test]
    fn chain_stops_on_retry_requested() {
        let chain = guard_sets::full();
        let prov = default_provenance();
        let intents = [Intent::LiteraryQuoteContext];
        let ctx = ctx("dune quote for iran conflict", &intents, &[], &prov);
        let result = chain.apply(
            "I cannot provide quotes related to ongoing conflicts.".into(),
            &ctx,
        );
        assert!(result.retry.is_some());
        let retry = result.retry.unwrap();
        assert_eq!(retry.guard_id, GuardId::LiteraryQuoteRetry);
        assert!(retry.resume_index > 0);
    }

    // -- DeclaredActionGuard --

    #[test]
    fn declared_action_detects_imperative() {
        use crate::api::routes::agent::guard_impls::behavioral::detect_declared_action;
        assert!(detect_declared_action("attack the goblin").is_some());
        assert!(detect_declared_action("stab the merchant in the back").is_some());
        assert!(detect_declared_action("I'll pull my sword and strike").is_some());
        assert!(detect_declared_action("please cast fireball at the door").is_some());
        assert!(detect_declared_action("climb the tower wall").is_some());
    }

    #[test]
    fn declared_action_ignores_analytical_verbs() {
        use crate::api::routes::agent::guard_impls::behavioral::detect_declared_action;
        assert!(detect_declared_action("analyze the logs").is_none());
        assert!(detect_declared_action("summarize the report").is_none());
        assert!(detect_declared_action("find the root cause").is_none());
        assert!(detect_declared_action("check the deployment status").is_none());
        assert!(detect_declared_action("compare these two files").is_none());
        assert!(detect_declared_action("run the tests").is_none());
        assert!(detect_declared_action("search the room for traps").is_none());
        assert!(detect_declared_action("create a new project").is_none());
        assert!(detect_declared_action("delete the old backup").is_none());
    }

    #[test]
    fn declared_action_ignores_questions() {
        use crate::api::routes::agent::guard_impls::behavioral::detect_declared_action;
        assert!(detect_declared_action("what is the goblin doing?").is_none());
        assert!(detect_declared_action("how do I search for traps?").is_none());
        assert!(detect_declared_action("can you help me?").is_none());
        assert!(detect_declared_action("where are we?").is_none());
    }

    #[test]
    fn declared_action_guard_passes_when_resolved() {
        let prov = default_provenance();
        let intents = [];
        let ctx = ctx("attack the hooded figure", &intents, &[], &prov);
        let guard = DeclaredActionGuard;
        let verdict = guard.evaluate(
            "You lunge at the hooded figure with your sword. Roll d20 + 5 for your attack.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn declared_action_guard_retries_when_ignored() {
        let prov = default_provenance();
        let intents = [];
        let ctx = ctx("stab the merchant in the back", &intents, &[], &prov);
        let guard = DeclaredActionGuard;
        let verdict = guard.evaluate(
            "The tavern is warm and inviting. You sit down and order an ale. The bard plays a tune.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::RetryRequested { .. }));
    }

    #[test]
    fn declared_action_guard_passes_consequence_surfacing() {
        let prov = default_provenance();
        let intents = [];
        let ctx = ctx("attack the king", &intents, &[], &prov);
        let guard = DeclaredActionGuard;
        let verdict = guard.evaluate(
            "Before proceeding — are you sure? The king's guards will react immediately.",
            &ctx,
        );
        assert!(matches!(verdict, GuardVerdict::Pass));
    }

    #[test]
    fn declared_action_guard_retries_draw_sword_attack_when_not_referenced() {
        let prov = default_provenance();
        let intents = [];
        let ctx = ctx("I draw my sword and attack", &intents, &[], &prov);
        let guard = DeclaredActionGuard;
        let verdict = guard.evaluate(
            "The dungeon corridor stretches ahead. Torchlight flickers on the walls.",
            &ctx,
        );
        assert!(
            matches!(verdict, GuardVerdict::RetryRequested { .. }),
            "expected RetryRequested when declared action (draw/attack) is not resolved"
        );
    }

    #[test]
    fn declared_action_guard_passes_conversational_analytical_request() {
        let prov = default_provenance();
        let intents = [];
        let ctx = ctx("analyze the data", &intents, &[], &prov);
        let guard = DeclaredActionGuard;
        let verdict = guard.evaluate(
            "Here is a breakdown of the dataset: the mean is 42, the median is 38.",
            &ctx,
        );
        assert!(
            matches!(verdict, GuardVerdict::Pass),
            "expected Pass for analytical request — 'analyze' is not a declared physical action"
        );
    }

    #[test]
    fn chain_resume_from_continues_remaining_guards() {
        let chain = guard_sets::full();
        let prov = default_provenance();
        let intents = [Intent::LiteraryQuoteContext];
        let ctx = ctx("dune quote for iran conflict", &intents, &[], &prov);
        let result = chain.apply_from(
            "Fear is the mind-killer. In this context, resist panic.".into(),
            &ctx,
            5,
        );
        assert!(result.retry.is_none());
        assert!(result.content.contains("Fear is the mind-killer"));
    }
}
