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

//! MCP namespace standard-policy handlers and governance helpers.

use crate::identity::sentinels;
use crate::mcp::param_names;
use crate::mcp::registry::McpTool;
use crate::models::GovernancePolicy;
use crate::models::field_names;
use crate::{db, validate};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};

// --- D1.4 (#985): per-tool McpTool impls for the three namespace-
// standard governance tools ---

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_set_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceSetStandardRequest {
    /// Namespace.
    pub namespace: String,

    /// Standard memory id.
    pub id: String,

    /// Inherit-from namespace.
    #[serde(default)]
    pub parent: Option<String>,

    /// Task 1.8 policy in metadata.governance.
    #[serde(default)]
    pub governance: Option<serde_json::Value>,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_set_standard`.
#[allow(dead_code)]
pub struct NamespaceSetStandardTool;

impl McpTool for NamespaceSetStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_SET_STANDARD
    }
    fn description() -> &'static str {
        "Set a memory as the standard/policy for a namespace."
    }
    fn docs() -> &'static str {
        "Standard memory auto-prepended to recall + session_start. Rule layering: global '*' + parent chain + namespace. Task 1.8: governance policy merged into metadata. P4/G1: inherit flag."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceSetStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_get_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceGetStandardRequest {
    /// Namespace.
    pub namespace: String,

    /// Task 1.6: return full inheritance chain.
    #[serde(default)]
    pub inherit: Option<bool>,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_get_standard`.
#[allow(dead_code)]
pub struct NamespaceGetStandardTool;

impl McpTool for NamespaceGetStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_GET_STANDARD
    }
    fn description() -> &'static str {
        "Get the standard/policy memory for a namespace."
    }
    fn docs() -> &'static str {
        "Returns the standard. inherit=true (Task 1.6) returns the resolved chain (global '*' -> ancestors -> namespace)."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceGetStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

/// v0.7.0 #972 D1.4 (#985) — request body for `memory_namespace_clear_standard`.
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct NamespaceClearStandardRequest {
    /// Namespace.
    pub namespace: String,
}

/// v0.7.0 #972 D1.4 (#985) — `McpTool` impl for `memory_namespace_clear_standard`.
#[allow(dead_code)]
pub struct NamespaceClearStandardTool;

impl McpTool for NamespaceClearStandardTool {
    fn name() -> &'static str {
        crate::mcp::registry::tool_names::MEMORY_NAMESPACE_CLEAR_STANDARD
    }
    fn description() -> &'static str {
        "Clear the standard/policy for a namespace."
    }
    fn docs() -> &'static str {
        "Clear the namespace standard."
    }
    fn input_schema() -> Value {
        crate::mcp::registry::input_schema_for::<NamespaceClearStandardRequest>()
    }
    fn family() -> &'static str {
        crate::profile::Family::Governance.name()
    }
}

pub fn handle_namespace_set_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;
    let id = params["id"]
        .as_str()
        .ok_or(crate::errors::msg::ID_REQUIRED)?;
    validate::validate_id(id).map_err(|e| e.to_string())?;
    let parent = params["parent"].as_str();
    if let Some(p) = parent {
        validate::validate_namespace(p).map_err(|e| e.to_string())?;
    }

    // #913 (security-medium / SOC2, 2026-05-19) — admin governance
    // audit. Namespace-standard mutations gate every downstream write;
    // the forensic-chain row MUST land before the storage write so the
    // audit trail captures intent even on validate/storage failure
    // downstream. MCP callers resolve via `identity::resolve_agent_id`.
    let caller = crate::identity::resolve_agent_id(params["agent_id"].as_str(), None)
        .unwrap_or_else(|_| sentinels::ANONYMOUS_INVALID.to_string());
    crate::governance::audit::record_decision(
        &caller,
        "allow",
        "namespace_set_standard",
        "",
        serde_json::json!({
            "namespace": namespace,
            (field_names::STANDARD_ID): id,
            "parent": parent,
            "has_governance": params.get(param_names::GOVERNANCE).is_some_and(|v| !v.is_null()),
        }),
    );

    // #929 SECURITY-high (Track A P6, 2026-05-20) — ownership gate on
    // the MCP entry. Mirrors the HTTP handler gate at
    // `handlers/hook_subscribers.rs::set_namespace_standard_inner`.
    // Without this gate any MCP caller could overwrite any namespace's
    // governance policy by passing the existing standard's id —
    // sibling vector to the HTTP path.
    //
    // Gate scope: ONLY applies when the caller has explicitly claimed
    // identity via `params["agent_id"]`. When agent_id is absent the
    // caller is identifying as the daemon process itself (the
    // historical "no claim, no gate" semantic — used by integration
    // tests that invoke this MCP function as a library call without
    // setting up a full identity chain, and by daemon-internal
    // bootstrap paths). The HTTP entry is the load-bearing gate for
    // external callers because it ALWAYS resolves caller via
    // X-Agent-Id (anonymous fallback included) and threads the
    // resolved caller into the MCP params before calling here (see
    // `set_namespace_standard_inner` line 715-720). Direct MCP
    // callers that DO claim identity (via stdio JSON-RPC params)
    // remain gated by this check; daemon-internal callers and tests
    // that don't claim identity get the legacy posture.
    let identity_claimed = params
        .get(param_names::AGENT_ID)
        .and_then(|v| v.as_str())
        .is_some_and(|s| !s.is_empty());
    if identity_claimed && let Ok(Some(existing_mem)) = db::get(conn, id) {
        let recorded_owner = existing_mem
            .metadata
            .get(param_names::AGENT_ID)
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let is_unowned = recorded_owner.is_empty() || recorded_owner == "system";
        if !is_unowned && recorded_owner != caller && caller != sentinels::DAEMON_PRINCIPAL {
            return Err(format!(
                "caller does not own this namespace standard (caller={caller}, owner={recorded_owner})"
            ));
        }
        // Unowned-legacy claim — rewrite metadata.agent_id to caller
        // so subsequent calls are gated. No-op when caller is the
        // anonymous fallback (don't anchor ownership to anonymous).
        if is_unowned && !caller.is_empty() && caller != sentinels::ANONYMOUS_INVALID {
            let mut new_meta = if existing_mem.metadata.is_object() {
                existing_mem.metadata.clone()
            } else {
                serde_json::json!({})
            };
            if let Some(obj) = new_meta.as_object_mut() {
                obj.insert(
                    "agent_id".to_string(),
                    serde_json::Value::String(caller.clone()),
                );
                obj.entry("scope".to_string())
                    .or_insert_with(|| serde_json::Value::String("shared".to_string()));
            }
            // Best-effort: don't fail the set-standard call if the
            // claim rewrite fails — the ownership gate will refire on
            // the next call once metadata.agent_id is non-empty.
            if let Err(e) = db::update(
                conn,
                id,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                Some(&new_meta),
            ) {
                tracing::warn!(
                    "namespace_standard (MCP): ownership-claim metadata update failed: {e}"
                );
            }
        }
    }

    // Task 1.8: optional governance policy merged into the standard memory's
    // metadata.governance. Policy is deserialized + validated before write.
    //
    // v0.7.0 G-PHASE-E-2 (#707) — DO NOT strip "extra" fields from the
    // governance blob. The pre-#707 path round-tripped the incoming
    // governance JSON through the typed `GovernancePolicy` struct, which
    // only carries the whitelist (write/promote/delete/approver/inherit/
    // max_reflection_depth). Any other key — most notably
    // `require_approval_above_depth`, which is a free-function look-up
    // (`storage::resolve_require_approval_above_depth`) outside the
    // typed struct — was silently dropped on re-serialisation. Operators
    // who set `require_approval_above_depth` on a memory and later
    // touched `memory_namespace_set_standard` for any reason lost that
    // gate without any error or log.
    //
    // The fix: take the existing standard memory's `metadata.governance`
    // (if any) as the base, layer the incoming `g` on top key-by-key,
    // validate the merged blob's typed shape, and write the FULL merged
    // JSON back — so unknown-to-the-struct fields on either side survive
    // the round-trip.
    let governance_val = params.get(param_names::GOVERNANCE).filter(|v| !v.is_null());
    if let Some(g) = governance_val {
        // Load the standard memory first so we can read its existing
        // governance blob and merge.
        let mut mem = db::get(conn, id)
            .map_err(|e| e.to_string())?
            .ok_or_else(|| crate::errors::msg::memory_not_found(id))?;
        // Compute the merged governance JSON: existing fields preserved,
        // incoming overrides applied per-key.
        let merged = merge_governance_fields(mem.metadata.get(param_names::GOVERNANCE), g);
        // Validate the typed shape of the result. Deserialising drops
        // unknown fields but the typed sub-set must still parse + pass
        // policy validation — this catches operator typos in known
        // fields without rejecting extras like
        // `require_approval_above_depth`.
        let policy: crate::models::GovernancePolicy = serde_json::from_value(merged.clone())
            .map_err(|e| crate::errors::msg::invalid(param_names::GOVERNANCE, e))?;
        validate::validate_governance_policy(&policy).map_err(|e| e.to_string())?;

        let mut metadata = if mem.metadata.is_object() {
            mem.metadata.clone()
        } else {
            serde_json::json!({})
        };
        if let Some(obj) = metadata.as_object_mut() {
            obj.insert(crate::META_KEY_GOVERNANCE.to_string(), merged);
        }
        let (found, _) = db::update(
            conn,
            &mem.id,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(&metadata),
        )
        .map_err(|e| e.to_string())?;
        if !found {
            return Err(format!("memory not found during governance merge: {id}"));
        }
        mem.metadata = metadata;
    }

    db::set_namespace_standard(conn, namespace, id, parent).map_err(|e| e.to_string())?;
    let mut resp = json!({"set": true, "namespace": namespace, (field_names::STANDARD_ID): id});
    if let Some(p) = parent {
        resp["parent"] = json!(p);
    }
    if let Some(g) = governance_val {
        resp[field_names::GOVERNANCE] = g.clone();
    }
    Ok(resp)
}

pub fn handle_namespace_get_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;

    // Task 1.6: --inherit returns the full resolved chain, most-general-first.
    let inherit = params["inherit"].as_bool().unwrap_or(false);
    if inherit {
        let chain = super::build_namespace_chain(conn, namespace);
        let mut standards: Vec<Value> = Vec::new();
        for link in &chain {
            if let Some(std) = super::lookup_namespace_standard(conn, link) {
                let gov = extract_governance(&std);
                let entry = json!({
                    "namespace": link,
                    (field_names::STANDARD_ID): std["id"].clone(),
                    "title": std["title"].clone(),
                    "content": std["content"].clone(),
                    "priority": std["priority"].clone(),
                    (field_names::GOVERNANCE): gov,
                });
                standards.push(entry);
            }
        }
        return Ok(json!({
            "namespace": namespace,
            "chain": chain,
            "standards": standards,
            "count": standards.len(),
        }));
    }

    let standard_id = db::get_namespace_standard(conn, namespace).map_err(|e| e.to_string())?;
    match standard_id {
        Some(id) => {
            let mem = db::get(conn, &id).map_err(|e| e.to_string())?;
            match mem {
                Some(m) => {
                    // v0.7.0 #1326 — surface the FULL `metadata.governance`
                    // blob, not just the typed `GovernancePolicy` whitelist.
                    // The typed struct omits caller-supplied free fields
                    // like `require_approval_above_depth` (read by
                    // `storage::resolve_require_approval_above_depth`)
                    // which means set_standard would silently round-trip
                    // them into the DB while get_standard returned a
                    // policy blob that didn't surface them. The fix:
                    // start from the typed policy as the base (so default
                    // fields like `write`/`promote`/`delete`/`approver`/
                    // `inherit` populate), then layer the raw blob keys
                    // back on top — preserving every field the operator
                    // sent on set, including off-struct fields.
                    let gov = merge_governance_for_response(&m.metadata);
                    Ok(json!({
                        "namespace": namespace,
                        (field_names::STANDARD_ID): id,
                        "title": m.title,
                        "content": m.content,
                        "priority": m.priority,
                        (field_names::GOVERNANCE): gov,
                    }))
                }
                None => Ok(
                    json!({"namespace": namespace, (field_names::STANDARD_ID): id, "warning": "standard memory not found — may have been deleted"}),
                ),
            }
        }
        None => Ok(json!({"namespace": namespace, (field_names::STANDARD_ID): null})),
    }
}

/// v0.7.0 #1326 — build the governance blob returned by the
/// `memory_namespace_get_standard` MCP response.
///
/// Composes the typed [`GovernancePolicy`] default-or-resolved
/// serialisation as the base (so well-known fields like `write`,
/// `promote`, `delete`, `approver`, `inherit` always populate even
/// when omitted by the operator), then overlays the raw
/// `metadata.governance` JSON keys on top so caller-supplied
/// off-struct fields (`require_approval_above_depth`,
/// `skill_promotion_min_depth`, future extension keys) survive the
/// round-trip.
///
/// The pre-#1326 path returned only the typed-struct serialisation,
/// dropping every off-struct field on the floor. The L1-8 approval
/// gate (`storage::resolve_require_approval_above_depth`) read the
/// field directly from `metadata.governance`, so the gate kept firing
/// correctly — but operators inspecting the get-standard surface saw
/// an incomplete policy blob and could not confirm their gate was
/// stored.
fn merge_governance_for_response(metadata: &Value) -> Value {
    // Step 1: typed-struct base. Resolves to defaults when no
    // governance metadata is present (matches pre-#1326 behaviour
    // for the well-known-fields surface).
    //
    // #1384 — observability for stored-corruption on the GET path.
    // `from_metadata` returns three shapes: `None` (no governance
    // metadata, fall through to default), `Some(Ok(policy))` (typed
    // parse succeeded), or `Some(Err(parse_err))` (governance JSON
    // present but malformed — e.g. an unknown enum variant landed via
    // direct SQL update or a stale binary). Pre-#1384 the
    // `.map(Result::unwrap_or_default)` collapsed the `Err` arm
    // silently into `GovernancePolicy::default()`, so the get-standard
    // response showed `write: "any"` (the default) even when the
    // stored row carried `write: "approval"` (an invalid variant the
    // operator typo'd). Surface the drift via a tracing WARN so the
    // silent fallback is observable in operator logs; preserve the
    // backward-compat default-on-failure shape because the response
    // body must always include a typed policy block.
    let base = match GovernancePolicy::from_metadata(metadata) {
        None => GovernancePolicy::default(),
        Some(Ok(p)) => p,
        Some(Err(parse_err)) => {
            tracing::warn!(
                target: "ai_memory::governance::policy_read",
                error = %parse_err,
                "stored metadata.governance failed typed deserialise on \
                 the get-standard read path — the response will fall \
                 back to GovernancePolicy::default() (write=any, \
                 delete=owner, approver=human). Likely cause: direct \
                 SQL update, older binary, or a typo'd enum variant \
                 that bypassed the set-standard typed gate. Operator \
                 should re-run `memory_namespace_set_standard` to \
                 restore the typed shape."
            );
            GovernancePolicy::default()
        }
    };
    let mut merged = serde_json::to_value(&base)
        .ok()
        .and_then(|v| v.as_object().cloned())
        .unwrap_or_default();

    // Step 2: overlay raw `metadata.governance` keys so off-struct
    // fields survive. Iterate over the raw JSON object and insert
    // every key that wasn't already produced by the typed serialisation
    // (keys present in both are byte-equal because the typed struct
    // was deserialised from the same JSON).
    if let Some(raw) = metadata
        .get(param_names::GOVERNANCE)
        .and_then(Value::as_object)
    {
        for (k, v) in raw {
            merged.entry(k.clone()).or_insert_with(|| v.clone());
        }
    }
    Value::Object(merged)
}

/// Task 1.8 — extract metadata.governance from a serialized memory value,
/// resolving to the default policy when missing or invalid. Used by the
/// `--inherit` get-standard path and tool responses.
///
/// v0.7.0 #1326 — surfaces the FULL `metadata.governance` JSON blob
/// (typed default fields + caller-supplied off-struct fields like
/// `require_approval_above_depth`). The pre-#1326 path returned only
/// the typed [`GovernancePolicy`] whitelist, dropping every off-struct
/// field. See [`merge_governance_for_response`] for the merge contract.
pub(super) fn extract_governance(mem_val: &Value) -> Value {
    let Some(meta) = mem_val.get(param_names::METADATA) else {
        return serde_json::to_value(GovernancePolicy::default()).unwrap_or(Value::Null);
    };
    merge_governance_for_response(meta)
}

pub(crate) fn handle_namespace_clear_standard(
    conn: &rusqlite::Connection,
    params: &Value,
) -> Result<Value, String> {
    let namespace = params["namespace"]
        .as_str()
        .ok_or(crate::errors::msg::NAMESPACE_REQUIRED)?;
    validate::validate_namespace(namespace).map_err(|e| e.to_string())?;

    // #913 (security-medium / SOC2, 2026-05-19) — admin governance audit.
    let caller = crate::identity::resolve_agent_id(params["agent_id"].as_str(), None)
        .unwrap_or_else(|_| sentinels::ANONYMOUS_INVALID.to_string());

    // #1777 — owner gate, MIRRORING the #929 SET gate above. Clearing a
    // namespace's governance STANDARD reverts it to permissive allow-on-silence,
    // disarming the delete/write/promote gates that protect EVERY memory in the
    // namespace — so it must be gated identically to SETTING it (else a
    // non-owner could disarm protections others rely on, and a non-admin owner
    // could set-but-never-clear). Gate ONLY when identity is claimed
    // (params.agent_id present); daemon-internal + unclaimed callers pass, same
    // as SET. The bare DELETE carries no owner, but the standard memory's
    // metadata.agent_id is recoverable via get_namespace_standard → db::get.
    let identity_claimed = params
        .get(param_names::AGENT_ID)
        .and_then(|v| v.as_str())
        .is_some_and(|s| !s.is_empty());
    if identity_claimed
        && let Ok(Some(standard_id)) = db::get_namespace_standard(conn, namespace)
        && let Ok(Some(existing_mem)) = db::get(conn, &standard_id)
    {
        let recorded_owner = existing_mem
            .metadata
            .get(param_names::AGENT_ID)
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let is_unowned = recorded_owner.is_empty() || recorded_owner == "system";
        if !is_unowned && recorded_owner != caller && caller != sentinels::DAEMON_PRINCIPAL {
            return Err(format!(
                "caller does not own this namespace standard (caller={caller}, owner={recorded_owner})"
            ));
        }
    }

    crate::governance::audit::record_decision(
        &caller,
        "allow",
        "namespace_clear_standard",
        "",
        serde_json::json!({ "namespace": namespace }),
    );

    let cleared = db::clear_namespace_standard(conn, namespace).map_err(|e| e.to_string())?;
    Ok(json!({"cleared": cleared, "namespace": namespace}))
}

/// Auto-register namespace parent chain from the filesystem path.
/// Walks from cwd up to home dir, checks if each directory name has a namespace
/// standard set, and registers the parent chain.
///
/// Example: cwd = /home/user/monorepo/frontend
///   → checks "frontend" (cwd), "monorepo" (parent), stops at home dir
///   → if "monorepo" has a standard, sets `parent_namespace` of "frontend" to "monorepo"
#[allow(dead_code)]
pub(super) fn auto_register_path_hierarchy(conn: &rusqlite::Connection, namespace: &str) {
    // Only run if this namespace doesn't already have an explicit parent
    if db::get_namespace_parent(conn, namespace).is_some() {
        return;
    }
    let Ok(cwd) = std::env::current_dir() else {
        return;
    };
    let home = dirs::home_dir().unwrap_or_default();
    // Walk up from parent of cwd (cwd itself IS the namespace)
    let mut current = cwd.parent().map(std::path::Path::to_path_buf);
    while let Some(dir) = current {
        // Stop at or above home directory
        if dir == home || !dir.starts_with(&home) {
            break;
        }
        if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) {
            // Check if this directory name has a namespace standard
            if db::get_namespace_standard(conn, dir_name)
                .ok()
                .flatten()
                .is_some()
            {
                // Found a parent with a standard — register it
                let now = chrono::Utc::now().to_rfc3339();
                let _ = conn.execute(
                    "UPDATE namespace_meta SET parent_namespace = ?1, updated_at = ?2 WHERE namespace = ?3 AND parent_namespace IS NULL",
                    rusqlite::params![dir_name, now, namespace],
                );
                tracing::info!(
                    "auto-registered parent namespace: {} -> {}",
                    namespace,
                    dir_name
                );
                break;
            }
        }
        current = dir.parent().map(std::path::Path::to_path_buf);
    }
}

/// v0.7.0 G-PHASE-E-2 (#707) — merge an incoming governance JSON blob
/// onto an existing one, key-by-key. The incoming blob's keys override
/// the existing ones; keys present only on the existing blob (e.g. an
/// operator-set `require_approval_above_depth`) survive untouched.
///
/// Both sides are treated as JSON objects — non-object inputs (or
/// missing `existing`) collapse to "use the incoming side wholesale".
/// Returns a fresh `serde_json::Value::Object` so the caller can
/// re-serialise without aliasing the input slots.
fn merge_governance_fields(
    existing: Option<&serde_json::Value>,
    incoming: &serde_json::Value,
) -> serde_json::Value {
    let mut merged = serde_json::Map::new();
    if let Some(existing_obj) = existing.and_then(serde_json::Value::as_object) {
        for (k, v) in existing_obj {
            merged.insert(k.clone(), v.clone());
        }
    }
    if let Some(incoming_obj) = incoming.as_object() {
        for (k, v) in incoming_obj {
            merged.insert(k.clone(), v.clone());
        }
    } else {
        // Incoming is not an object — fall back to the incoming value
        // wholesale so callers can still pass primitives through (the
        // typed deserialise on the caller side will reject anything
        // structurally wrong).
        return incoming.clone();
    }
    serde_json::Value::Object(merged)
}

// ---------------------------------------------------------------------------
// Archive tool handlers
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    //! Coverage C-2 — focused tests for the namespace-standard MCP handlers
    //! and the private `extract_governance` helper.

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

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

    fn insert_one(conn: &rusqlite::Connection, ns: &str, title: &str) -> String {
        let now = chrono::Utc::now().to_rfc3339();
        let mem = Memory {
            id: uuid::Uuid::new_v4().to_string(),
            tier: Tier::Long,
            namespace: ns.to_string(),
            title: title.to_string(),
            content: format!("body for {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!({}),
            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,
        };
        db::insert(conn, &mem).expect("insert")
    }

    fn insert_owned(conn: &rusqlite::Connection, ns: &str, title: &str, owner: &str) -> String {
        let now = chrono::Utc::now().to_rfc3339();
        let mem = Memory {
            id: uuid::Uuid::new_v4().to_string(),
            tier: Tier::Long,
            namespace: ns.to_string(),
            title: title.to_string(),
            content: format!("body for {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": owner }),
            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,
        };
        db::insert(conn, &mem).expect("insert")
    }

    #[test]
    fn clear_standard_owner_gate_1777() {
        // #1777 — clearing a namespace standard mirrors the SET #929 owner gate:
        // a claimed cross-owner caller is refused; the owner (and an unclaimed
        // caller) may clear.
        let conn = fresh_conn();
        let id = insert_owned(&conn, "ns-clear-1777", "standard", "ai:alice");
        handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "ns-clear-1777", "id": id, "agent_id": "ai:alice"}),
        )
        .expect("alice sets the standard");

        // ai:bob (claimed, ≠ owner) is refused; the standard remains.
        let err = handle_namespace_clear_standard(
            &conn,
            &json!({"namespace": "ns-clear-1777", "agent_id": "ai:bob"}),
        )
        .expect_err("cross-owner clear must be refused");
        assert!(err.contains("does not own"), "got: {err}");
        assert!(
            db::get_namespace_standard(&conn, "ns-clear-1777")
                .unwrap()
                .is_some(),
            "standard must survive a refused cross-owner clear"
        );

        // The owner ai:alice CAN clear it.
        let ok = handle_namespace_clear_standard(
            &conn,
            &json!({"namespace": "ns-clear-1777", "agent_id": "ai:alice"}),
        );
        assert!(ok.is_ok(), "owner clear must succeed: {ok:?}");
        assert!(
            db::get_namespace_standard(&conn, "ns-clear-1777")
                .unwrap()
                .is_none(),
            "owner clear removes the standard"
        );
    }

    // set_standard: happy path without governance.
    #[test]
    fn set_standard_happy_path() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-a", "standard");
        let resp = handle_namespace_set_standard(&conn, &json!({"namespace": "ns-a", "id": id}))
            .expect("ok");
        assert_eq!(resp["set"], true);
    }

    // set_standard: with parent.
    #[test]
    fn set_standard_with_parent_echoed() {
        let conn = fresh_conn();
        // Seed the parent standard first.
        let parent_id = insert_one(&conn, "parent", "p-standard");
        db::set_namespace_standard(&conn, "parent", &parent_id, None).unwrap();
        let id = insert_one(&conn, "child", "c-standard");
        let resp = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "child", "id": id, "parent": "parent"}),
        )
        .expect("ok");
        assert_eq!(resp["parent"].as_str(), Some("parent"));
    }

    // set_standard: missing namespace → typed error.
    #[test]
    fn set_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(&conn, &json!({"id": "x"})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // set_standard: missing id → typed error.
    #[test]
    fn set_standard_missing_id_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(&conn, &json!({"namespace": "x"})).unwrap_err();
        assert!(err.contains("id"), "got: {err}");
    }

    // set_standard: invalid namespace rejected (validate_namespace).
    #[test]
    fn set_standard_invalid_namespace_rejected() {
        let conn = fresh_conn();
        let err =
            handle_namespace_set_standard(&conn, &json!({"namespace": "has spaces", "id": "x"}))
                .unwrap_err();
        assert!(!err.is_empty());
    }

    // set_standard: invalid parent namespace rejected.
    #[test]
    fn set_standard_invalid_parent_rejected() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-parent-bad", "p");
        let err = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "ns-parent-bad", "id": id, "parent": "has spaces"}),
        )
        .unwrap_err();
        assert!(!err.is_empty());
    }

    // set_standard: with governance — merged into metadata + echoed.
    #[test]
    fn set_standard_with_governance_merged() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-gov", "p");
        // GovernancePolicy schema requires `write`; other fields default.
        let governance = json!({"write": "any"});
        let resp = handle_namespace_set_standard(
            &conn,
            &json!({"namespace": "ns-gov", "id": id, "governance": governance.clone()}),
        )
        .expect("ok");
        assert_eq!(resp["governance"], governance);
        // The merged metadata must round-trip through db::get.
        let mem = db::get(&conn, &id).unwrap().unwrap();
        assert!(mem.metadata.get("governance").is_some());
    }

    // set_standard: invalid governance (deserialization fails).
    #[test]
    fn set_standard_with_invalid_governance_rejected() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-bad-gov", "p");
        let err = handle_namespace_set_standard(
            &conn,
            &json!({
                "namespace": "ns-bad-gov",
                "id": id,
                "governance": "this is not an object",
            }),
        )
        .unwrap_err();
        assert!(err.contains("invalid governance"), "got: {err}");
    }

    // set_standard: governance specified but memory id does not exist.
    #[test]
    fn set_standard_with_governance_unknown_id_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_set_standard(
            &conn,
            &json!({
                "namespace": "ns-missing-id",
                "id": "11111111-2222-3333-4444-555555555555",
                "governance": {"write": "any"},
            }),
        )
        .unwrap_err();
        assert!(err.contains("memory not found"), "got: {err}");
    }

    // get_standard: missing namespace → typed error.
    #[test]
    fn get_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_get_standard(&conn, &json!({})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // get_standard: unknown namespace → standard_id null.
    #[test]
    fn get_standard_unknown_namespace_returns_null() {
        let conn = fresh_conn();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "no-such"})).expect("ok");
        assert!(resp["standard_id"].is_null());
    }

    // get_standard: happy path.
    #[test]
    fn get_standard_happy_path() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-get", "got");
        db::set_namespace_standard(&conn, "ns-get", &id, None).unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "ns-get"})).expect("ok");
        assert_eq!(resp["standard_id"].as_str(), Some(id.as_str()));
        assert_eq!(resp["title"].as_str(), Some("got"));
        // governance defaults filled in.
        assert!(resp["governance"].is_object());
    }

    // get_standard: standard_id present but memory deleted — warning surfaced.
    #[test]
    fn get_standard_dangling_id_surfaces_warning() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-dangling", "g");
        db::set_namespace_standard(&conn, "ns-dangling", &id, None).unwrap();
        // Now physically delete the memory row.
        conn.execute("DELETE FROM memories WHERE id = ?1", rusqlite::params![&id])
            .unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "ns-dangling"})).expect("ok");
        assert!(resp["warning"].is_string());
    }

    // get_standard: --inherit returns chain + standards array.
    #[test]
    fn get_standard_inherit_returns_chain() {
        let conn = fresh_conn();
        let global_id = insert_one(&conn, "*", "global");
        db::set_namespace_standard(&conn, "*", &global_id, None).unwrap();
        let leaf_id = insert_one(&conn, "leaf-ns", "leaf");
        db::set_namespace_standard(&conn, "leaf-ns", &leaf_id, None).unwrap();
        let resp =
            handle_namespace_get_standard(&conn, &json!({"namespace": "leaf-ns", "inherit": true}))
                .expect("ok");
        assert!(resp["chain"].is_array());
        assert!(resp["count"].as_u64().unwrap() >= 1);
    }

    // clear_standard: happy.
    #[test]
    fn clear_standard_happy() {
        let conn = fresh_conn();
        let id = insert_one(&conn, "ns-clear", "c");
        db::set_namespace_standard(&conn, "ns-clear", &id, None).unwrap();
        let resp =
            handle_namespace_clear_standard(&conn, &json!({"namespace": "ns-clear"})).expect("ok");
        assert_eq!(resp["cleared"], true);
    }

    // clear_standard: missing namespace → error.
    #[test]
    fn clear_standard_missing_namespace_errors() {
        let conn = fresh_conn();
        let err = handle_namespace_clear_standard(&conn, &json!({})).unwrap_err();
        assert!(err.contains("namespace"), "got: {err}");
    }

    // clear_standard: invalid namespace rejected.
    #[test]
    fn clear_standard_invalid_namespace_rejected() {
        let conn = fresh_conn();
        let err = handle_namespace_clear_standard(&conn, &json!({"namespace": "has spaces"}))
            .unwrap_err();
        assert!(!err.is_empty());
    }

    // extract_governance: empty metadata returns default policy.
    #[test]
    fn extract_governance_default_when_missing_metadata() {
        let val = json!({"id": "x"});
        let gov = extract_governance(&val);
        assert!(gov.is_object());
    }

    // extract_governance: full metadata with valid governance.
    #[test]
    fn extract_governance_round_trips_valid_policy() {
        let val = json!({
            "metadata": {
                "governance": {"min_priority": 0}
            }
        });
        let gov = extract_governance(&val);
        assert!(
            gov.is_object(),
            "expected default-or-resolved policy object"
        );
    }

    // auto_register_path_hierarchy: no-op when parent already set.
    #[test]
    fn auto_register_noop_when_parent_already_set() {
        let conn = fresh_conn();
        // Seed parent_namespace via direct SQL to bypass auto-detect.
        conn.execute(
            "INSERT INTO namespace_meta (namespace, standard_id, updated_at, parent_namespace)
             VALUES ('child-ns', NULL, '2026-01-01T00:00:00Z', 'set-parent')",
            [],
        )
        .unwrap();
        // Should be a no-op: assert the function does not panic and the
        // parent_namespace value survives.
        auto_register_path_hierarchy(&conn, "child-ns");
        let p: Option<String> = conn
            .query_row(
                "SELECT parent_namespace FROM namespace_meta WHERE namespace = 'child-ns'",
                [],
                |r| r.get(0),
            )
            .unwrap();
        assert_eq!(p.as_deref(), Some("set-parent"));
    }

    // auto_register_path_hierarchy: no namespace_meta row → no-op (no panic).
    #[test]
    fn auto_register_handles_missing_row_gracefully() {
        let conn = fresh_conn();
        // Should not panic when nothing is set up.
        auto_register_path_hierarchy(&conn, "non-existent-ns");
    }
}

#[cfg(test)]
mod d1_4_985_tests {
    //! D1.4 (#985) — schema-parity for `memory_namespace_set_standard`,
    //! `memory_namespace_get_standard`, `memory_namespace_clear_standard`.
    use super::*;
    use crate::mcp::d1_4_985_helpers::{
        assert_descriptions_match, assert_property_set_parity, derived_props_for,
    };

    #[test]
    fn memory_namespace_set_standard_parity_985() {
        let derived = derived_props_for::<NamespaceSetStandardRequest>();
        assert_property_set_parity("memory_namespace_set_standard", &derived);
        assert_descriptions_match("memory_namespace_set_standard", &derived);
    }

    #[test]
    fn memory_namespace_set_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceSetStandardTool::name(),
            "memory_namespace_set_standard"
        );
        assert_eq!(NamespaceSetStandardTool::family(), "governance");
    }

    #[test]
    fn memory_namespace_get_standard_parity_985() {
        let derived = derived_props_for::<NamespaceGetStandardRequest>();
        assert_property_set_parity("memory_namespace_get_standard", &derived);
        assert_descriptions_match("memory_namespace_get_standard", &derived);
    }

    #[test]
    fn memory_namespace_get_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceGetStandardTool::name(),
            "memory_namespace_get_standard"
        );
        assert_eq!(NamespaceGetStandardTool::family(), "governance");
    }

    #[test]
    fn memory_namespace_clear_standard_parity_985() {
        let derived = derived_props_for::<NamespaceClearStandardRequest>();
        assert_property_set_parity("memory_namespace_clear_standard", &derived);
        assert_descriptions_match("memory_namespace_clear_standard", &derived);
    }

    #[test]
    fn memory_namespace_clear_standard_tool_metadata_985() {
        assert_eq!(
            NamespaceClearStandardTool::name(),
            "memory_namespace_clear_standard"
        );
        assert_eq!(NamespaceClearStandardTool::family(), "governance");
    }
}
