// Copyright 2026 AlphaOne LLC
// SPDX-License-Identifier: Apache-2.0

//! MCP `memory_delete` handler.

use crate::mcp::VectorIndex;
use crate::mcp::param_names;
use crate::mcp::registry::McpTool;
use crate::{db, validate};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};
use std::path::Path;

// --- D1.6 (#987): per-tool McpTool impl for `memory_delete` (lifecycle family) ---

/// v0.7.0 #972 D1.6 (#987) — request body for `memory_delete`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct DeleteRequest {
    pub id: String,
}

/// v0.7.0 #972 D1.6 (#987) — `McpTool` impl for `memory_delete`.
#[allow(dead_code)]
pub struct DeleteTool;

impl McpTool for DeleteTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_DELETE
    }
    fn description() -> &'static str {
        "Delete a memory by ID."
    }
    fn docs() -> &'static str {
        "Hard-delete by id (removes row, embedding, FTS, links). Use memory_forget for bulk pattern delete (archives first)."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<DeleteRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Lifecycle.name()
    }
}

#[cfg(test)]
mod d1_6_987_tests {
    //! D1.6 (#987) — schema parity for `memory_delete`.
    //! Shared helpers live at [`crate::mcp::parity_test_helpers`].
    use super::*;
    use crate::mcp::parity_test_helpers::{
        assert_descriptions_match, assert_property_set_parity, derived_props_for,
    };

    #[test]
    fn delete_parity_987() {
        let derived = derived_props_for::<DeleteRequest>();
        assert_property_set_parity("memory_delete", &derived);
        assert_descriptions_match("memory_delete", &derived);
    }

    #[test]
    fn delete_tool_metadata_987() {
        assert_eq!(DeleteTool::name(), "memory_delete");
        assert_eq!(DeleteTool::family(), "lifecycle");
    }
}

pub(super) fn handle_delete(
    conn: &rusqlite::Connection,
    db_path: &Path,
    params: &Value,
    vector_index: Option<&VectorIndex>,
    mcp_client: Option<&str>,
) -> Result<Value, String> {
    let id = params["id"]
        .as_str()
        .ok_or(crate::errors::msg::ID_REQUIRED)?;
    validate::validate_id(id).map_err(|e| e.to_string())?;

    // #913 (security-medium / SOC2, 2026-05-19) — admin/destructive
    // state-change audit. MCP `memory_delete` is the canonical
    // destructive operation; emit the forensic-chain row BEFORE the
    // permission gate + storage write so the audit trail captures the
    // caller's intent regardless of downstream outcome. Complementary
    // to `audit::emit(AuditAction::Delete)` further down which writes
    // the SIEM-shaped enterprise row AFTER the delete commits.
    let caller_for_forensic =
        crate::identity::resolve_agent_id(params["agent_id"].as_str(), mcp_client)
            .unwrap_or_else(|_| crate::identity::sentinels::ANONYMOUS_INVALID.to_string());
    crate::governance::audit::record_decision(
        &caller_for_forensic,
        "allow",
        crate::mcp::registry::tool_names::MEMORY_DELETE,
        "",
        json!({ "id": id }),
    );

    // Resolve the memory first so governance has owner context.
    let target = if let Some(m) = db::get(conn, id).map_err(|e| e.to_string())? {
        Some(m)
    } else {
        db::get_by_prefix(conn, id).map_err(|e| e.to_string())?
    };
    let Some(target) = target else {
        return Err(crate::errors::msg::MEMORY_NOT_FOUND.into());
    };

    // P5 (G9): snapshot fields the dispatcher needs BEFORE delete frees
    // the row. The dispatch itself is fire-and-forget after the DELETE
    // commits, but the payload is built from this owned snapshot.
    let snapshot_namespace = target.namespace.clone();
    let snapshot_title = target.title.clone();
    let snapshot_tier = target.tier.as_str().to_string();
    let snapshot_owner: Option<String> = target
        .metadata
        .get(param_names::AGENT_ID)
        .and_then(|v| v.as_str())
        .map(str::to_string);

    // v0.7.0 K9 — unified permission pipeline (delete-side).
    {
        use crate::permissions::{Op, PermissionContext, Permissions};
        let agent_id = crate::identity::resolve_agent_id(params["agent_id"].as_str(), mcp_client)
            .map_err(|e| e.to_string())?;
        let payload = json!({"id": target.id, "title": target.title});
        let ctx = PermissionContext {
            op: Op::MemoryDelete,
            namespace: target.namespace.clone(),
            agent_id,
            payload,
        };
        match Permissions::evaluate(&ctx, &[]) {
            crate::permissions::Decision::Allow | crate::permissions::Decision::Modify(_) => {}
            crate::permissions::Decision::Deny(reason) => {
                return Err(crate::governance::deny_message(
                    "delete",
                    crate::governance::DenyGate::PermissionRule,
                    &reason,
                ));
            }
            crate::permissions::Decision::Ask(prompt) => {
                return Ok(json!({
                    "status": "ask",
                    "reason": prompt,
                    "action": "delete",
                    "memory_id": target.id,
                }));
            }
        }
    }

    // Task 1.9: governance enforcement (delete-side).
    {
        use crate::models::{GovernanceDecision, GovernedAction};
        let agent_id = crate::identity::resolve_agent_id(params["agent_id"].as_str(), mcp_client)
            .map_err(|e| e.to_string())?;
        let mem_owner = target
            .metadata
            .get(param_names::AGENT_ID)
            .and_then(|v| v.as_str())
            .map(str::to_string);
        let payload = json!({"id": target.id, "title": target.title});
        match db::enforce_governance(
            conn,
            GovernedAction::Delete,
            &target.namespace,
            &agent_id,
            Some(&target.id),
            mem_owner.as_deref(),
            &payload,
        )
        .map_err(|e| e.to_string())?
        {
            GovernanceDecision::Allow => {}
            GovernanceDecision::Deny(refusal) => {
                return Err(crate::governance::deny_message(
                    "delete",
                    crate::governance::DenyGate::Governance,
                    &refusal.reason,
                ));
            }
            GovernanceDecision::Pending(pending_id) => {
                // v0.7.0 K4 — see the store-side companion call.
                crate::subscriptions::dispatch_approval_requested(conn, &pending_id, db_path);
                return Ok(json!({
                    "status": "pending",
                    "pending_id": pending_id,
                    "reason": crate::errors::msg::GOVERNANCE_REQUIRES_APPROVAL,
                    "action": "delete",
                    "memory_id": target.id,
                }));
            }
        }
    }

    // #1786 — owner gate: refuse a cross-owner delete. The MCP surface calls
    // raw `db::delete` directly (bypassing the SAL trait + the HTTP handler's
    // `require_caller_owns_memory`), so without this a multi-tenant caller could
    // delete another agent's private memory by id. The gate is keyed on the
    // ENFORCED-read caller (`resolve_read_visibility_caller`, env-only): it
    // fires ONLY when `AI_MEMORY_AGENT_ID` is set (the multi-tenant opt-in),
    // matching the read-path ownership posture (#1468/#1720 B1) so the
    // single-operator trust-all default is byte-unchanged. Lenient (unstamped /
    // self-owned / daemon / inbox-target pass); `allow_inbox = true` mirrors the
    // HTTP delete-side gate.
    if let Some(caller) = crate::identity::resolve_read_visibility_caller() {
        if !crate::visibility::caller_owns_for_mutation(&target, &caller, true) {
            return Err(crate::errors::msg::CALLER_DOES_NOT_OWN_MEMORY.into());
        }
    }

    let deleted = db::delete(conn, &target.id).map_err(|e| e.to_string())?;
    if deleted {
        if let Some(idx) = vector_index {
            idx.remove(&target.id);
        }
        // PR-5 (issue #487): security audit trail. No-op when disabled.
        crate::audit::emit(crate::audit::EventBuilder::new(
            crate::audit::AuditAction::Delete,
            crate::audit::actor(
                snapshot_owner
                    .clone()
                    .unwrap_or_else(|| "unknown".to_string()),
                mcp_client.map_or(crate::audit::synthesis_sources::HOST_FALLBACK, |_| {
                    crate::audit::synthesis_sources::MCP_CLIENT_INFO
                }),
                None,
            ),
            crate::audit::target_memory(
                target.id.clone(),
                snapshot_namespace.clone(),
                Some(snapshot_title.clone()),
                Some(snapshot_tier.clone()),
                None,
            ),
        ));
        // P5 (G9): fire `memory_delete` webhook AFTER the row is gone
        // (best-effort, fire-and-forget — same pattern as memory_store).
        let details = serde_json::to_value(crate::subscriptions::DeleteEventDetails {
            title: snapshot_title,
            tier: snapshot_tier,
        })
        .ok();
        crate::subscriptions::dispatch_event_with_details(
            conn,
            crate::mcp::registry::tool_names::MEMORY_DELETE,
            &target.id,
            &snapshot_namespace,
            snapshot_owner.as_deref(),
            db_path,
            details,
        );
        Ok(json!({"deleted": true}))
    } else {
        Err(crate::errors::msg::MEMORY_NOT_FOUND.into())
    }
}

#[cfg(test)]
mod tests {
    //! L0.7-3 Tier B chunk-A — coverage tests for `handle_delete`.
    //!
    //! Six-category template:
    //! A. happy path — full id + prefix resolution, response & DB side effect
    //! B. validation — missing / invalid id
    //! D. state-dependent — id not present
    //! E. idempotency — second delete returns not-found
    //! F. audit chain — emit() called (no-op without sink, but the path is taken)

    use super::*;
    use crate::models::{Memory, Tier};
    use crate::storage as db;

    fn fresh_conn() -> rusqlite::Connection {
        db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
    }

    fn db_path() -> std::path::PathBuf {
        std::path::PathBuf::from(":memory:")
    }

    fn make_mem(title: &str, ns: &str) -> Memory {
        let now = chrono::Utc::now().to_rfc3339();
        Memory {
            id: uuid::Uuid::new_v4().to_string(),
            tier: Tier::Mid,
            namespace: ns.to_string(),
            title: title.to_string(),
            content: format!("c {title}"),
            tags: vec![],
            priority: 5,
            confidence: 1.0,
            source: "test".to_string(),
            access_count: 0,
            created_at: now.clone(),
            updated_at: now,
            last_accessed_at: None,
            expires_at: None,
            metadata: json!({"agent_id": "ai:alice"}),
            reflection_depth: 0,
            memory_kind: crate::models::MemoryKind::Observation,
            entity_id: None,
            persona_version: None,
            citations: Vec::new(),
            source_uri: None,
            source_span: None,
            confidence_source: crate::models::ConfidenceSource::CallerProvided,
            confidence_signals: None,
            confidence_decayed_at: None,
            version: 1,
            lifecycle_state: crate::models::LifecycleState::Open,
        }
    }

    // A. happy path — full id
    #[test]
    fn happy_path_deletes_full_id() {
        let conn = fresh_conn();
        let mem = make_mem("doomed", "test");
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        let out =
            handle_delete(&conn, &db_path, &json!({"id": id.clone()}), None, None).expect("ok");
        assert_eq!(out["deleted"].as_bool(), Some(true));
        // DB side effect
        assert!(db::get(&conn, &id).unwrap().is_none(), "row removed");
    }

    // A. happy path — prefix resolution (no exact-id match, prefix matches)
    #[test]
    fn happy_path_prefix_resolution() {
        let conn = fresh_conn();
        let mut mem = make_mem("prefixed", "test");
        mem.id = "abcdef01-aaaa-bbbb-cccc-ddddeeeeffff".to_string();
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        let out = handle_delete(&conn, &db_path, &json!({"id": "abcdef01"}), None, None)
            .expect("prefix delete");
        assert_eq!(out["deleted"].as_bool(), Some(true));
        assert!(db::get(&conn, &id).unwrap().is_none());
    }

    // A. happy path — vector_index None branch (skipped) and Some-branch via API
    // The Some-branch needs a VectorIndex; we exercise it minimally below.
    #[test]
    fn happy_path_with_vector_index_removes_entry() {
        use crate::hnsw::VectorIndex;
        let conn = fresh_conn();
        let mem = make_mem("vec-target", "test");
        let id = db::insert(&conn, &mem).expect("insert");
        let idx = VectorIndex::empty();
        idx.insert(id.clone(), vec![0.1; 384]);
        let db_path = db_path();
        let out = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id.clone()}),
            Some(&idx),
            Some("ai:claude-code"),
        )
        .expect("delete");
        assert_eq!(out["deleted"].as_bool(), Some(true));
    }

    // B. missing id
    #[test]
    fn missing_id_returns_error() {
        let conn = fresh_conn();
        let db_path = db_path();
        let err = handle_delete(&conn, &db_path, &json!({}), None, None).unwrap_err();
        assert!(err.contains("id is required"));
    }

    // B. invalid id format
    #[test]
    fn invalid_id_format_rejected() {
        let conn = fresh_conn();
        let db_path = db_path();
        let err = handle_delete(&conn, &db_path, &json!({"id": ""}), None, None).unwrap_err();
        assert!(!err.is_empty());
    }

    // D. unknown id
    #[test]
    fn unknown_id_returns_not_found() {
        let conn = fresh_conn();
        let db_path = db_path();
        let err = handle_delete(
            &conn,
            &db_path,
            &json!({"id": "deadbeef-1234-5678-9abc-def012345678"}),
            None,
            None,
        )
        .unwrap_err();
        assert!(err.contains("not found"));
    }

    // E. idempotency: deleting twice errors the second time
    #[test]
    fn double_delete_errors_second_time() {
        let conn = fresh_conn();
        let mem = make_mem("twice", "test");
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        let _ = handle_delete(&conn, &db_path, &json!({"id": id.clone()}), None, None)
            .expect("first delete");
        let err = handle_delete(&conn, &db_path, &json!({"id": id}), None, None).unwrap_err();
        assert!(err.contains("not found"));
    }

    // F. audit chain — emit is called via the audit module (no sink installed in
    // tests, so emission is a no-op, but the call path is exercised and covered).
    // We assert by re-fetching via list and confirming the row is gone — proving
    // the audit/emit codepath ran inline.
    #[test]
    fn happy_path_drives_audit_emit_call_path() {
        let conn = fresh_conn();
        let mem = make_mem("audit", "test");
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        // Pass an explicit agent_id to drive the resolve_agent_id branch
        let out = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id, "agent_id": "ai:caller"}),
            None,
            Some("ai:claude-code"),
        )
        .expect("delete");
        assert_eq!(out["deleted"].as_bool(), Some(true));
    }

    // K9 / governance paths mutate the process-wide ACTIVE_PERMISSION_RULES
    // AND the process-wide PermissionsMode atomic. We hold BOTH locks for
    // the duration so concurrent tests don't race either knob:
    //   - `lock_permissions_mode_for_test` (config) — gates ACTIVE mode
    //   - `SHARED_PERMISSION_RULES_GUARD` (mcp/mod) — gates ACTIVE rules
    // The scope guard pins mode=Advisory and clears both registries on
    // drop so any panic mid-test leaves the next test seeing the default.
    fn lock_rules() -> std::sync::MutexGuard<'static, ()> {
        crate::mcp::SHARED_PERMISSION_RULES_GUARD
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    struct RulesScope {
        _rules: std::sync::MutexGuard<'static, ()>,
        _mode: std::sync::MutexGuard<'static, ()>,
    }
    impl Drop for RulesScope {
        fn drop(&mut self) {
            crate::permissions::clear_active_permission_rules_for_test();
            crate::config::clear_permissions_mode_override_for_test();
        }
    }
    fn rules_scope() -> RulesScope {
        let mode = crate::config::lock_permissions_mode_for_test();
        let rules = lock_rules();
        crate::permissions::clear_active_permission_rules_for_test();
        // Advisory keeps Ask as Ask (Enforce escalates Ask → Deny) and
        // still enforces explicit Deny rules.
        crate::config::override_active_permissions_mode_for_test(
            crate::config::PermissionsMode::Advisory,
        );
        RulesScope {
            _rules: rules,
            _mode: mode,
        }
    }

    // C. K9 Deny path
    #[test]
    fn k9_deny_rule_short_circuits() {
        use crate::permissions::{PermissionRule, RuleDecision, set_active_permission_rules};
        let _g = rules_scope();
        let conn = fresh_conn();
        let mem = make_mem("deny-target", "k9-deny-delete");
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        set_active_permission_rules(vec![PermissionRule {
            namespace_pattern: "k9-deny-delete".to_string(),
            op: "memory_delete".to_string(),
            agent_pattern: "*".to_string(),
            decision: RuleDecision::Deny,
            reason: Some("denied".to_string()),
        }]);
        let err = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id, "agent_id": "ai:caller"}),
            None,
            None,
        )
        .unwrap_err();
        assert!(err.contains("denied"), "got: {err}");
    }

    // C. K9 Ask path — returns structured envelope, not error
    #[test]
    fn k9_ask_rule_returns_ask_envelope() {
        use crate::permissions::{PermissionRule, RuleDecision, set_active_permission_rules};
        let _g = rules_scope();
        let conn = fresh_conn();
        let mem = make_mem("ask-target", "k9-ask-delete");
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        set_active_permission_rules(vec![PermissionRule {
            namespace_pattern: "k9-ask-delete".to_string(),
            op: "memory_delete".to_string(),
            agent_pattern: "*".to_string(),
            decision: RuleDecision::Ask,
            reason: Some("operator approval required".to_string()),
        }]);
        let out = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id, "agent_id": "ai:caller"}),
            None,
            None,
        )
        .expect("ask returns Ok");
        assert_eq!(out["status"].as_str(), Some("ask"));
        assert_eq!(out["action"].as_str(), Some("delete"));
    }

    // Helper: install a governance policy on `ns` that gates `delete`
    // at the given `delete_level`. The standard memory carries an
    // explicit `agent_id` so Owner-level checks have a target.
    fn install_delete_policy(
        conn: &rusqlite::Connection,
        ns: &str,
        delete_level: crate::models::GovernanceLevel,
        approver: crate::models::ApproverType,
        owner: &str,
    ) {
        use crate::models::{CorePolicy, GovernancePolicy, default_metadata};
        let policy = GovernancePolicy {
            core: CorePolicy {
                delete: delete_level,
                approver,
                ..CorePolicy::default()
            },
            ..Default::default()
        };
        let now = chrono::Utc::now().to_rfc3339();
        let mut metadata = default_metadata();
        if let Some(obj) = metadata.as_object_mut() {
            obj.insert(
                "agent_id".to_string(),
                serde_json::Value::String(owner.to_string()),
            );
            obj.insert(
                "governance".to_string(),
                serde_json::to_value(&policy).unwrap(),
            );
        }
        let standard = Memory {
            id: uuid::Uuid::new_v4().to_string(),
            tier: Tier::Long,
            namespace: format!("_standards-{ns}"),
            title: format!("std-{ns}"),
            content: "policy".to_string(),
            tags: vec![],
            priority: 9,
            confidence: 1.0,
            source: "test".to_string(),
            access_count: 0,
            created_at: now.clone(),
            updated_at: now,
            last_accessed_at: None,
            expires_at: None,
            metadata,
            reflection_depth: 0,
            memory_kind: crate::models::MemoryKind::Observation,
            entity_id: None,
            persona_version: None,
            citations: Vec::new(),
            source_uri: None,
            source_span: None,
            confidence_source: crate::models::ConfidenceSource::CallerProvided,
            confidence_signals: None,
            confidence_decayed_at: None,
            version: 1,
            lifecycle_state: crate::models::LifecycleState::Open,
        };
        let sid = db::insert(conn, &standard).expect("insert standard");
        db::set_namespace_standard(conn, ns, &sid, None).expect("set standard");
    }

    // Governance Deny path (lines 93-94): owner-level delete by
    // non-owner. Requires Enforce mode (Advisory just logs).
    #[test]
    fn governance_deny_blocks_delete() {
        let _gate = crate::config::lock_permissions_mode_for_test();
        crate::config::override_active_permissions_mode_for_test(
            crate::config::PermissionsMode::Enforce,
        );
        let conn = fresh_conn();
        let ns = "gov-deny-del";
        install_delete_policy(
            &conn,
            ns,
            crate::models::GovernanceLevel::Owner,
            crate::models::ApproverType::Human,
            "ai:alice",
        );
        let mut mem = make_mem("target", ns);
        if let Some(obj) = mem.metadata.as_object_mut() {
            obj.insert(
                "agent_id".to_string(),
                serde_json::Value::String("ai:alice".to_string()),
            );
        }
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        let err = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id, "agent_id": "ai:eve"}),
            None,
            None,
        )
        .unwrap_err();
        assert!(
            err.contains("governance") || err.contains("denied") || err.contains("owner"),
            "got: {err}"
        );
        crate::config::clear_permissions_mode_override_for_test();
    }

    // Governance Pending path (lines 96-105): Approve policy queues a
    // pending action and returns an envelope. Requires Enforce mode.
    #[test]
    fn governance_pending_returns_pending_envelope() {
        let _gate = crate::config::lock_permissions_mode_for_test();
        crate::config::override_active_permissions_mode_for_test(
            crate::config::PermissionsMode::Enforce,
        );
        let conn = fresh_conn();
        let ns = "gov-pending-del";
        install_delete_policy(
            &conn,
            ns,
            crate::models::GovernanceLevel::Approve,
            crate::models::ApproverType::Human,
            "ai:alice",
        );
        let mem = make_mem("target", ns);
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();
        let out = handle_delete(
            &conn,
            &db_path,
            &json!({"id": id, "agent_id": "ai:bob"}),
            None,
            None,
        )
        .expect("pending returns Ok");
        assert_eq!(out["status"].as_str(), Some("pending"));
        assert_eq!(out["action"].as_str(), Some("delete"));
        assert!(out["pending_id"].as_str().is_some());
        crate::config::clear_permissions_mode_override_for_test();
    }

    fn agent_id_env_lock() -> &'static std::sync::Mutex<()> {
        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
        LOCK.get_or_init(|| std::sync::Mutex::new(()))
    }

    #[test]
    fn cross_owner_delete_refused_1786() {
        // #1786 — when AI_MEMORY_AGENT_ID is SET (the multi-tenant opt-in), the
        // MCP delete owner gate refuses a cross-owner delete and still lets the
        // owner delete. (Unset = trust-all single-tenant default, no gate — that
        // path is covered by every other handle_delete test, which run without
        // the env set and still pass.)
        let _envg = agent_id_env_lock()
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let _pm = crate::config::lock_permissions_mode_for_test();
        crate::config::override_active_permissions_mode_for_test(
            crate::config::PermissionsMode::Off,
        );
        let conn = fresh_conn();
        let mem = make_mem("alice-private", "owner-gate-1786"); // owner = ai:alice
        let id = db::insert(&conn, &mem).expect("insert");
        let db_path = db_path();

        // Caller ai:bob (≠ owner) → refused; the row stays live.
        unsafe { std::env::set_var("AI_MEMORY_AGENT_ID", "ai:bob") };
        let err = handle_delete(&conn, &db_path, &json!({"id": id}), None, None)
            .expect_err("cross-owner delete must be refused");
        assert!(err.contains("does not own"), "got: {err}");
        assert!(
            db::get(&conn, &id).unwrap().is_some(),
            "row must remain live after a refused cross-owner delete"
        );

        // Caller ai:alice (owner) → allowed.
        unsafe { std::env::set_var("AI_MEMORY_AGENT_ID", "ai:alice") };
        let ok = handle_delete(&conn, &db_path, &json!({"id": id}), None, None);
        assert!(ok.is_ok(), "owner delete must succeed: {ok:?}");
        assert!(
            db::get(&conn, &id).unwrap().is_none(),
            "owner delete removes the row"
        );

        unsafe { std::env::remove_var("AI_MEMORY_AGENT_ID") };
        crate::config::clear_permissions_mode_override_for_test();
    }
}
