use super::*;
use serde_json::{Value, json};

fn contains_object_key(value: &Value, key: &str) -> bool {
    match value {
        Value::Object(map) => {
            map.contains_key(key) || map.values().any(|child| contains_object_key(child, key))
        }
        Value::Array(items) => items.iter().any(|child| contains_object_key(child, key)),
        _ => false,
    }
}

#[test]
fn normalize_api_path_adds_leading_slash() {
    assert_eq!(normalize_api_path("v1/events").unwrap(), "/v1/events");
    assert_eq!(normalize_api_path("/v1/events").unwrap(), "/v1/events");
}

#[test]
fn admin_api_path_detection_is_strict_to_v1_admin_namespace() {
    assert!(is_admin_api_path("/v1/admin"));
    assert!(is_admin_api_path("/v1/admin/security/kill-switch"));
    assert!(!is_admin_api_path("/v1/agent/context"));
    assert!(!is_admin_api_path("/health"));
}

#[test]
fn initialize_instructions_describe_direct_agent_only_surface() {
    let server = McpServer::new(McpRuntimeConfig {
        api_url: "http://127.0.0.1:9".to_string(),
        no_auth: true,
        explicit_token: None,
        allow_admin: false,
    });

    let payload = server.initialize_payload_with_skillpack_brief(None);
    let instructions = payload
        .get("instructions")
        .and_then(Value::as_str)
        .expect("initialize payload should include instructions");

    assert!(instructions.contains("agent-only and intentionally minimal"));
    assert!(instructions.contains("record_activity"));
    assert!(instructions.contains("record_recovery"));
    assert!(instructions.contains("report_health"));
    assert!(instructions.contains("update_profile"));
    assert!(instructions.contains("update_plan"));
    assert!(instructions.contains("read_query"));
    assert!(instructions.contains("analyze"));
    assert!(instructions.contains("call analyze first with the best public request"));
    assert!(instructions.contains("do not use read_query or analyze_catalog as routine preflight"));
    assert!(instructions.contains("published schemas are the source of truth"));
    assert!(instructions.contains("canonical source_id values"));
    assert!(instructions.contains("status=success"));
    assert!(instructions.contains("user_notice_relay.status=relay_required"));
    assert!(instructions.contains("append_to_user_reply once"));
    assert!(!instructions.contains("kura_agent_brief"));
    assert!(!instructions.contains("kura_agent_context"));
}

#[test]
fn initialize_advertises_current_mcp_protocol_version() {
    let server = McpServer::new(McpRuntimeConfig {
        api_url: "http://127.0.0.1:9".to_string(),
        no_auth: true,
        explicit_token: None,
        allow_admin: false,
    });

    let payload = server.initialize_payload_with_skillpack_brief(None);

    assert_eq!(payload["protocolVersion"], "2025-06-18");
}

#[tokio::test]
async fn read_framed_json_accepts_lf_only_header_separator() {
    let payload = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
    let wire = format!("Content-Length: {}\n\n{}", payload.len(), payload);
    let (mut writer, reader) = tokio::io::duplex(1024);

    tokio::spawn(async move {
        writer.write_all(wire.as_bytes()).await.unwrap();
    });

    let mut reader = BufReader::new(reader);
    let parsed = read_framed_json(&mut reader)
        .await
        .expect("LF-framed message should parse")
        .expect("message should be present");

    assert_eq!(parsed["method"], "ping");
}

#[tokio::test]
async fn read_framed_json_accepts_mcp_ndjson_messages() {
    let wire = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n";
    let mut reader = BufReader::new(&wire[..]);

    let parsed = read_framed_json(&mut reader)
        .await
        .expect("NDJSON-framed MCP message should parse")
        .expect("message should be present");

    assert_eq!(parsed["method"], "ping");
}

#[tokio::test]
async fn write_framed_json_emits_mcp_ndjson_messages() {
    let (reader, mut writer) = tokio::io::duplex(1024);
    let response = json!({"jsonrpc": "2.0", "id": 1, "result": {}});

    write_framed_json(&mut writer, &response)
        .await
        .expect("response should write");
    drop(writer);

    let mut reader = BufReader::new(reader);
    let mut line = String::new();
    reader.read_line(&mut line).await.unwrap();

    assert!(line.ends_with('\n'));
    assert!(!line.starts_with("Content-Length"));
    let parsed: Value = serde_json::from_str(line.trim_end()).unwrap();
    assert_eq!(parsed["result"], json!({}));
}

#[test]
fn initialize_instructions_point_only_to_family_write_tools() {
    let server = McpServer::new(McpRuntimeConfig {
        api_url: "http://127.0.0.1:9".to_string(),
        no_auth: true,
        explicit_token: None,
        allow_admin: false,
    });

    let payload = server.initialize_payload_with_skillpack_brief(None);
    let instructions = payload
        .get("instructions")
        .and_then(Value::as_str)
        .expect("initialize payload should include instructions");

    assert!(instructions.contains("Harness profile: task_first_vnext."));
    assert!(instructions.contains("record_activity"));
    assert!(instructions.contains(
        "record_recovery/report_health/record_nutrition/record_measurement/record_benchmark"
    ));
    assert!(instructions.contains("update_profile and update_plan"));
    assert!(instructions.contains("Use only the published tools and their schemas"));
    assert!(!instructions.contains("family_write_turn"));
    assert!(!instructions.contains("write_training"));
    assert!(!instructions.contains("read_training_history"));
}

#[test]
fn initialize_instruction_helper_appends_skillpack_brief_when_supplied() {
    let instructions = append_skillpack_brief_to_initialize_instructions(
        "Base instructions.".to_string(),
        Some("Use analyze first."),
    );

    assert!(instructions.contains("Base instructions."));
    assert!(instructions.contains("Skillpack brief: Use analyze first."));
}

#[test]
fn initialize_instruction_helper_keeps_static_fallback_when_brief_absent() {
    let instructions =
        append_skillpack_brief_to_initialize_instructions("Base instructions.".to_string(), None);

    assert_eq!(instructions, "Base instructions.");
}

#[tokio::test]
async fn send_api_request_blocks_admin_paths_when_admin_not_allowed() {
    let server = McpServer::new(McpRuntimeConfig {
        api_url: "http://127.0.0.1:9".to_string(),
        no_auth: true,
        explicit_token: None,
        allow_admin: false,
    });

    let err = server
        .send_api_request(
            Method::GET,
            "/v1/admin/security/kill-switch",
            &[],
            None,
            false,
            false,
        )
        .await
        .expect_err("admin path should be blocked before network call");

    assert_eq!(err.code, "admin_path_blocked");
}

#[test]
fn parse_query_pairs_accepts_object_array_and_query_entries_alias() {
    let from_object = parse_query_pairs(
        Some(&json!({"event_type": "set.logged", "limit": 10})),
        "query",
    )
    .unwrap();
    assert_eq!(from_object.len(), 2);

    let from_array = parse_query_pairs(
        Some(&json!([
            {"key": "event_type", "value": "set.logged"},
            {"key": "limit", "value": 10}
        ])),
        "query",
    )
    .unwrap();
    assert_eq!(from_array.len(), 2);

    let mut args = Map::new();
    args.insert(
        "query_entries".to_string(),
        json!([
            {"key": "event_type", "value": "set.logged"},
            {"key": "limit", "value": 10}
        ]),
    );
    let from_alias = parse_query_pairs_from_args(&args).unwrap();
    assert_eq!(from_alias.len(), 2);

    args.insert("query".to_string(), json!({"event_type": "set.logged"}));
    let err = parse_query_pairs_from_args(&args)
        .expect_err("query and query_entries together must be rejected");
    assert_eq!(err.code, "validation_failed");
}

#[test]
fn extract_openapi_endpoints_sorts_and_compacts() {
    let spec = json!({
        "paths": {
            "/v1/events": {
                "post": { "summary": "Create event", "security": [{"bearer_auth": []}] },
                "get": { "summary": "List events", "security": [{"bearer_auth": []}] }
            },
            "/health": {
                "get": { "summary": "Health check" }
            }
        }
    });
    let endpoints = extract_openapi_endpoints(&spec);
    assert_eq!(endpoints.len(), 3);
    assert_eq!(endpoints[0]["path"], "/health");
    assert_eq!(endpoints[1]["method"], "GET");
    assert_eq!(endpoints[2]["method"], "POST");
}

#[test]
fn compact_openapi_section_omits_full_openapi_body() {
    let result = ApiCallResult {
        status: 200,
        body: json!({
            "openapi": "3.0.3",
            "info": {
                "title": "Kura API",
                "version": "2026-02-17"
            },
            "paths": {
                "/v1/events": {
                    "get": { "summary": "List events", "security": [{"bearer_auth": []}] }
                }
            }
        }),
        headers: None,
    };

    let compact = compact_openapi_section(&result);
    assert_eq!(compact["ok"], true);
    assert_eq!(compact["status"], 200);
    assert_eq!(compact["summary"]["title"], "Kura API");
    assert_eq!(compact["summary"]["version"], "2026-02-17");
    assert_eq!(compact["summary"]["endpoint_count"], 1);
    assert!(compact.get("body").is_none());
}

#[test]
fn exercise_vocabulary_tool_schema_has_expected_bounds() {
    let tool = tool_definitions()
        .into_iter()
        .find(|tool| tool.name == "kura_exercise_vocabulary_discover")
        .expect("kura_exercise_vocabulary_discover tool must exist");
    let props = tool
        .input_schema
        .get("properties")
        .and_then(Value::as_object)
        .expect("tool schema properties must exist");
    assert_eq!(props["limit"]["minimum"], 1);
    assert_eq!(props["limit"]["maximum"], 200);
    assert!(tool.input_schema.get("required").is_none());
}

#[test]
fn agent_answer_admissibility_tool_keeps_contract_inputs() {
    let defs = tool_definitions();
    let admissibility_tool = defs
        .iter()
        .find(|tool| tool.name == "kura_agent_answer_admissibility")
        .expect("kura_agent_answer_admissibility tool must exist");
    assert_eq!(
        admissibility_tool.input_schema["required"],
        json!(["task_intent", "draft_answer"])
    );
}

#[test]
fn projection_list_schema_exposes_cursor_inputs() {
    let tool = tool_definitions()
        .into_iter()
        .find(|tool| tool.name == "kura_projection_list")
        .expect("kura_projection_list tool must exist");
    let props = tool
        .input_schema
        .get("properties")
        .and_then(Value::as_object)
        .expect("tool schema properties must exist");
    assert!(props.contains_key("projection_type"));
    assert!(props.contains_key("limit"));
    assert!(props.contains_key("cursor"));
}

#[test]
fn events_list_schema_exposes_auto_pagination_inputs() {
    let tool = tool_definitions()
        .into_iter()
        .find(|tool| tool.name == "kura_events_list")
        .expect("kura_events_list tool must exist");
    let props = tool
        .input_schema
        .get("properties")
        .and_then(Value::as_object)
        .expect("tool schema properties must exist");
    assert!(props.contains_key("auto_paginate"));
    assert!(props.contains_key("max_pages"));
    assert!(props.contains_key("max_total"));
}

#[test]
fn events_list_pagination_next_call_contract_requires_cursor_and_has_more() {
    let next = build_events_list_pagination_next_call(
        Some("set.logged"),
        Some("2026-03-01T00:00:00Z"),
        Some("2026-03-06T00:00:00Z"),
        Some(200),
        true,
        Some("cursor-1"),
    );
    assert_eq!(next["tool"], "kura_events_list");
    assert_eq!(next["arguments"]["cursor"], "cursor-1");
    assert_eq!(next["arguments"]["event_type"], "set.logged");
    assert_eq!(next["arguments"]["limit"], 200);
    assert_eq!(
        build_events_list_pagination_next_call(
            Some("set.logged"),
            None,
            None,
            Some(50),
            true,
            None
        ),
        Value::Null
    );
    assert_eq!(
        build_events_list_pagination_next_call(
            Some("set.logged"),
            None,
            None,
            Some(50),
            false,
            Some("cursor-2")
        ),
        Value::Null
    );
}

#[test]
fn tool_definitions_include_system_manifest_tools() {
    let tools = tool_definitions();
    assert!(
        tools.iter().any(|tool| tool.name == "kura_system_manifest"),
        "system manifest tool must be exposed"
    );
    assert!(
        tools
            .iter()
            .any(|tool| tool.name == "kura_system_section_get"),
        "system section fetch tool must be exposed"
    );
}

#[test]
fn public_tool_schema_hashes_cover_task_first_surface() {
    let hashes = tool_schema_hashes::public_tool_schema_hashes();

    for tool in [
        "read_query",
        "analyze",
        "record_activity",
        "record_recovery",
        "record_benchmark",
        "plan_activity",
        "update_profile",
        "update_plan",
    ] {
        assert!(hashes.contains_key(tool), "missing schema hash for {tool}");
        assert!(hashes[tool].starts_with("sha256:"));
    }
    assert!(tool_schema_hashes::public_tool_schema_bundle_hash().starts_with("sha256:"));
}

#[test]
fn internal_tool_definitions_remove_legacy_analysis_job_tools() {
    let tools = tool_definitions();
    assert!(
        tools
            .iter()
            .all(|tool| tool.name != "kura_analysis_job_create"),
        "legacy analysis create tool must not remain callable"
    );
    assert!(
        tools.iter().any(|tool| tool.name == "analyze"),
        "analysis tool must be exposed"
    );
    assert!(
        tools
            .iter()
            .all(|tool| tool.name != "kura_analysis_job_get"),
        "legacy analysis get tool must not remain callable"
    );
}

#[test]
fn analyze_tool_is_visible_and_unique() {
    // Phase-4 cutover guard (spec §6.4, test_457_*): the MCP surface
    // exposes exactly one canonical `analyze` tool. The archetype-era
    // `analyze_expert` and the transitional `analyze_invoke` are gone
    // — both from the published tools/list and from
    // `vnext_public_surface_tool_names`.
    let public = vnext_public_surface_tool_names();
    assert!(
        public.contains(&"analyze"),
        "analyze must be listed in vnext_public_surface_tool_names"
    );
    assert!(
        !public.contains(&"analyze_expert"),
        "analyze_expert was deleted in the Phase-4 cutover"
    );
    assert!(
        !public.contains(&"analyze_invoke"),
        "analyze_invoke was renamed to analyze in the Phase-4 cutover"
    );
    let published = published_tool_definitions();
    let analyze_entries = published
        .iter()
        .filter(|tool| tool.name == "analyze")
        .count();
    assert_eq!(
        analyze_entries, 1,
        "exactly one canonical analyze tool must be published"
    );
    assert!(
        !published.iter().any(|tool| tool.name == "analyze_expert"),
        "analyze_expert must not be listed in MCP tools/list"
    );
    assert!(
        !published.iter().any(|tool| tool.name == "analyze_invoke"),
        "analyze_invoke must not be listed in MCP tools/list"
    );
    let analyze = published
        .iter()
        .find(|tool| tool.name == "analyze")
        .expect("analyze tool must be exposed");
    assert!(analyze.description.contains("typed analyze envelope"));
    assert!(analyze.description.contains("POST /v4/agent/analyze/run"));
    assert!(!analyze.description.contains("plan_running"));
    assert!(!analyze.description.contains("resume_handle"));
    assert!(analyze.description.contains("Timeout is terminal"));
    let schema = &analyze.input_schema;
    assert_eq!(schema["type"], json!("object"));
    assert!(schema.get("required").is_none());
    let props = schema["properties"].as_object().expect("properties");
    assert!(props.contains_key("analysis_spec"));
    assert!(props.contains_key("semantic_request"));
    assert!(props.contains_key("canonical_request"));
    assert!(props.contains_key("audit_context"));
    assert!(
        !props.contains_key("intent"),
        "public MCP analyze schema must not expose direct `intent`"
    );
    assert!(
        !props.contains_key("request"),
        "typed MCP analyze schema must not expose the deleted public request object"
    );
    assert!(
        !props.contains_key("resume_handle"),
        "typed MCP analyze schema must not expose deleted resume handles"
    );
    assert_eq!(
        props["wait_timeout_ms"]["minimum"],
        json!(ANALYZE_WAIT_TIMEOUT_MS_MIN)
    );
    assert_eq!(
        props["wait_timeout_ms"]["maximum"],
        json!(ANALYZE_WAIT_TIMEOUT_MS_MAX)
    );
}

fn analyze_semantic_args(request: Value, wait_timeout_ms: Option<u64>) -> Map<String, Value> {
    let mut args = Map::new();
    args.insert("mode".to_string(), json!("resolve_and_run"));
    args.insert("semantic_request".to_string(), request);
    if let Some(ms) = wait_timeout_ms {
        args.insert("wait_timeout_ms".to_string(), json!(ms));
    }
    args
}

fn analyze_canonical_args(request: Value, wait_timeout_ms: Option<u64>) -> Map<String, Value> {
    let mut args = Map::new();
    args.insert("mode".to_string(), json!("run"));
    args.insert("canonical_request".to_string(), request);
    if let Some(ms) = wait_timeout_ms {
        args.insert("wait_timeout_ms".to_string(), json!(ms));
    }
    args
}

fn analyze_spec_args(request: Value, wait_timeout_ms: Option<u64>) -> Map<String, Value> {
    let mut args = Map::new();
    args.insert("mode".to_string(), json!("analysis_spec"));
    args.insert("analysis_spec".to_string(), request);
    if let Some(ms) = wait_timeout_ms {
        args.insert("wait_timeout_ms".to_string(), json!(ms));
    }
    args
}

#[test]
fn analyze_body_accepts_analysis_spec_request() {
    let args = analyze_spec_args(
        json!({
            "schema_version": "analysis_spec.v1",
            "question_type": "explanation",
            "variables": [
                {
                    "variable_id": "jump_height",
                    "role": "outcome",
                    "signal": "jump_height",
                    "source": "performance_tests",
                    "scope": {"protocol_variant": "free_arms"},
                    "time_alignment": {"anchor": "session_date"}
                }
            ]
        }),
        Some(8000),
    );
    let body = build_analyze_body(&args).expect("analysis_spec request must pass");
    assert_eq!(body["schema_version"], json!("analyze_envelope.v1"));
    assert_eq!(body["mode"], json!("analysis_spec"));
    assert_eq!(body["analysis_spec"]["question_type"], json!("explanation"));
    assert_eq!(body["wait_timeout_ms"], json!(8000));
    assert!(body.get("semantic_request").is_none());
    assert!(body.get("canonical_request").is_none());
}

#[test]
fn analyze_body_accepts_semantic_request() {
    let args = analyze_semantic_args(
        json!({
            "question_type": "trend",
            "outcome": {"signal": "sleep_quality"}
        }),
        Some(8000),
    );
    let body = build_analyze_body(&args).expect("semantic request must pass");
    assert_eq!(body["schema_version"], json!("analyze_envelope.v1"));
    assert_eq!(body["mode"], json!("resolve_and_run"));
    assert_eq!(body["semantic_request"]["question_type"], json!("trend"));
    assert_eq!(
        body["semantic_request"]["outcome"]["signal"],
        json!("sleep_quality")
    );
    assert_eq!(body["wait_timeout_ms"], json!(8000));
    assert!(body.get("intent").is_none());
    assert!(body.get("request").is_none());
}

#[test]
fn analyze_body_accepts_canonical_request() {
    let args = analyze_canonical_args(
        json!({
            "question_type": "trend",
            "scope_level": "domain_overview",
            "outcome": {"signal": "sleep_quality"}
        }),
        None,
    );
    let body = build_analyze_body(&args).expect("canonical request must pass");
    assert_eq!(body["mode"], json!("run"));
    assert_eq!(
        body["canonical_request"]["outcome"]["signal"],
        json!("sleep_quality")
    );
    assert!(body.get("semantic_request").is_none());
}

#[test]
fn analyze_body_includes_wait_timeout_when_within_bounds() {
    let args = analyze_semantic_args(
        json!({"question_type": "trend", "outcome": {"signal": "sleep_quality"}}),
        Some(8000),
    );
    let body = build_analyze_body(&args).expect("valid body");
    assert_eq!(body["wait_timeout_ms"], json!(8000));
}

#[test]
fn analyze_agent_runtime_adds_default_wait_timeout_without_changing_schema_builder() {
    let args = analyze_semantic_args(
        json!({
            "question_type": "trend",
            "outcome": {"signal": "sleep_quality"}
        }),
        None,
    );
    let mut body = build_analyze_body(&args).expect("valid body");
    assert!(body.get("wait_timeout_ms").is_none());

    prepare_agent_analyze_body(&mut body);

    assert_eq!(
        body["wait_timeout_ms"],
        json!(ANALYZE_AGENT_DEFAULT_WAIT_TIMEOUT_MS)
    );
}

#[test]
fn analyze_body_rejects_unknown_top_level_fields() {
    let mut args = analyze_semantic_args(
        json!({"question_type": "trend", "outcome": {"signal": "sleep_quality"}}),
        None,
    );
    args.insert("kernel".to_string(), json!({}));
    let err =
        build_analyze_body(&args).expect_err("kernel is an archetype-era field, must be rejected");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("kernel"));
}

#[test]
fn analyze_body_rejects_missing_request_object() {
    let args = Map::new();
    let err = build_analyze_body(&args).expect_err("missing request target must fail");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("analysis_spec"));
}

#[test]
fn analyze_body_rejects_both_request_objects() {
    let mut args = analyze_semantic_args(json!({"question_type": "trend"}), None);
    args.insert(
        "canonical_request".to_string(),
        json!({
            "question_type": "trend",
            "scope_level": "domain_overview",
            "outcome": {"signal": "sleep_quality"}
        }),
    );
    let err = build_analyze_body(&args).expect_err("both request objects must fail");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("analysis_spec"));
}

#[test]
fn analyze_body_rejects_direct_intent_field() {
    let mut args = analyze_semantic_args(
        json!({
            "question_type": "trend",
            "outcome": {"signal": "sleep_quality"}
        }),
        None,
    );
    args.insert(
        "intent".to_string(),
        json!({"question_type": "trend", "scope_level": "domain_overview"}),
    );
    let err = build_analyze_body(&args).expect_err("direct intent field must fail");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("intent"));
}

#[test]
fn analyze_body_rejects_deleted_resume_field() {
    let mut args = analyze_semantic_args(
        json!({"question_type": "trend", "outcome": {"signal": "sleep_quality"}}),
        None,
    );
    args.insert(
        "resume_handle".to_string(),
        json!({
            "kind": "analysis_job",
            "job_id": "018f3f7e-7cf0-7000-8000-000000000000"
        }),
    );
    let err = build_analyze_body(&args).expect_err("deleted resume field must fail");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("resume_handle"));
}

#[test]
fn analyze_body_rejects_mode_request_mismatch() {
    let mut args = analyze_semantic_args(json!({"question_type": "trend"}), None);
    args.insert("mode".to_string(), json!("run"));
    let err = build_analyze_body(&args).expect_err("run mode requires canonical request");
    assert_eq!(err.code, "validation_failed");
    assert_eq!(err.field.as_deref(), Some("mode"));
}

#[test]
fn analyze_body_rejects_wait_timeout_below_min() {
    let args = analyze_semantic_args(
        json!({"question_type": "trend", "outcome": {"signal": "sleep_quality"}}),
        Some(ANALYZE_WAIT_TIMEOUT_MS_MIN - 1),
    );
    let err = build_analyze_body(&args).expect_err("below-min wait must fail");
    assert_eq!(err.field.as_deref(), Some("wait_timeout_ms"));
}

#[test]
fn analyze_body_rejects_wait_timeout_above_max() {
    let args = analyze_semantic_args(
        json!({"question_type": "trend", "outcome": {"signal": "sleep_quality"}}),
        Some(ANALYZE_WAIT_TIMEOUT_MS_MAX + 1),
    );
    let err = build_analyze_body(&args).expect_err("above-max wait must fail");
    assert_eq!(err.field.as_deref(), Some("wait_timeout_ms"));
}

#[test]
fn import_and_provider_tools_hidden_by_default_runtime_profile() {
    if import_device_tools_enabled() {
        return;
    }
    let tools = tool_definitions();
    assert!(
        !tools
            .iter()
            .any(|tool| tool.name == "kura_import_job_create"),
        "import create tool should be hidden when profile flag is disabled"
    );
    assert!(
        !tools
            .iter()
            .any(|tool| tool.name == "kura_provider_connections_list"),
        "provider connection tool should be hidden when profile flag is disabled"
    );
}

#[test]
fn dedupe_scope_excludes_write_tools() {
    assert!(is_tool_call_dedupe_eligible("kura_projection_get"));
    assert!(is_tool_call_dedupe_eligible(
        "kura_exercise_vocabulary_discover"
    ));
    assert!(!is_tool_call_dedupe_eligible("write_training"));
    assert!(!is_tool_call_dedupe_eligible("family_write_turn"));
    assert!(!is_tool_call_dedupe_eligible("write_event_legacy"));
    assert!(!is_tool_call_dedupe_eligible("kura_import_job_create"));
    assert!(!is_tool_call_dedupe_eligible("kura_analysis_job_create"));
    assert!(!is_tool_call_dedupe_eligible("kura_analysis_job_get"));
    assert!(!is_tool_call_dedupe_eligible("kura_analysis_run"));
}

#[test]
fn dedupe_args_signature_is_order_stable_for_objects() {
    let mut args_a = Map::new();
    args_a.insert("alpha".to_string(), json!(1));
    args_a.insert("beta".to_string(), json!({"x": true, "y": 2}));

    let mut args_b = Map::new();
    args_b.insert("beta".to_string(), json!({"y": 2, "x": true}));
    args_b.insert("alpha".to_string(), json!(1));

    assert_eq!(
        stable_dedupe_args_signature(&args_a),
        stable_dedupe_args_signature(&args_b),
        "same semantic args should produce same dedupe signature regardless of key ordering"
    );
}

#[test]
fn dedupe_cache_returns_recent_entries_only() {
    let sid = format!("test-dedupe-{}", Uuid::now_v7());
    let args = Map::new();
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "kura_projection_get",
        "data": {"ok": true}
    });

    store_tool_call_dedupe_entry(&sid, "kura_projection_get", &args, &envelope);
    let hit = get_tool_call_dedupe_entry(&sid, "kura_projection_get", &args);
    assert!(hit.is_some(), "fresh entry must be deduped");

    let stale_key = tool_call_dedupe_key(&sid, "kura_projection_get", &args);
    {
        let mut cache = TOOL_CALL_DEDUPE_CACHE
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        cache.insert(
            stale_key,
            ToolCallDedupeEntry {
                created_at: Instant::now() - Duration::from_millis(TOOL_CALL_DEDUPE_WINDOW_MS + 10),
                envelope: json!({
                    "status": "complete",
                    "phase": "final",
                    "tool": "kura_projection_get",
                    "data": {"stale": true}
                }),
            },
        );
    }
    let stale_hit = get_tool_call_dedupe_entry(&sid, "kura_projection_get", &args);
    assert!(
        stale_hit.is_none(),
        "stale entry must not be used for dedupe response"
    );
}

#[test]
fn tool_text_content_inlines_large_payloads() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "read_query",
        "data": {
            "large_blob": "x".repeat(4000)
        }
    });

    let text = tool_text_content("read_query", &envelope);
    assert!(text.contains("large_blob"));
    assert!(!text.contains("structuredContent"));
}

#[test]
fn tool_text_content_keeps_small_payload_inline() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "read_query",
        "data": {
            "ok": true
        }
    });

    let text = tool_text_content("read_query", &envelope);
    assert_eq!(text, to_pretty_json(&envelope));
}

#[test]
fn flattened_public_tool_response_prefers_domain_body_and_continuation() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "read_query",
        "data": {
            "request": { "path": "/v4/agent/read-query" },
            "response": {
                "status": 200,
                "body": {
                    "items": [{"item_id": "a1"}],
                    "continuation": {
                        "has_more": true,
                        "cursor": "cursor-1"
                    }
                }
            },
            "contract": {
                "continuation": {
                    "status": "continue_on_same_tool",
                    "same_tool": "read_query",
                    "cursor": "cursor-1",
                    "agent_instruction": "Call read_query again with the returned continuation payload."
                }
            }
        },
        "runtime_observability": {
            "noise": true
        }
    });

    let flattened = flatten_public_tool_response("read_query", &envelope, false);
    assert_eq!(flattened["status"], "ok");
    assert_eq!(flattened["result"]["items"][0]["item_id"], "a1");
    assert_eq!(flattened["continuation"]["same_tool"], "read_query");
    assert_eq!(
        flattened["next_action"],
        "Call read_query again with the returned continuation payload."
    );
    assert!(flattened.get("runtime_observability").is_none());
    assert!(flattened.get("phase").is_none());
    assert!(flattened.get("tool").is_none());
}

#[test]
fn flattened_public_tool_response_propagates_response_form_metadata() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "read_query",
        "data": {
            "response": {
                "status": 200,
                "body": {
                    "response_form": "overview_recall",
                    "form_version": 1,
                    "response_family": "overview_recall",
                    "completeness_scope": "complete_as_an_overview_for_the_requested_window_not_as_a_full_raw_dump",
                    "response_kind": "collection",
                    "execution_mode": "retrieval",
                    "items": [{"item_id": "a1"}]
                }
            }
        }
    });

    let flattened = flatten_public_tool_response("read_query", &envelope, false);
    assert_eq!(flattened["response_form"], json!("overview_recall"));
    assert_eq!(flattened["form_version"], json!(1));
    assert_eq!(
        flattened["completeness_scope"],
        json!("complete_as_an_overview_for_the_requested_window_not_as_a_full_raw_dump")
    );
}

#[test]
fn flattened_analyze_answer_surfaces_stop_instruction() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "analyze",
        "data": {
            "response": {
                "status": 200,
                "body": {
                    "type": "answer",
                    "agent_usage": {
                        "answer_complete": true
                    },
                    "key_findings": ["sleep_quality improved"]
                }
            }
        }
    });

    let flattened = flatten_public_tool_response("analyze", &envelope, false);
    assert_eq!(
        flattened["next_action"],
        json!(
            "Stop tool use for this analysis and write the final answer from the analyze payload. Do not call read_query or analyze_catalog for enrichment unless the payload explicitly asks for a concrete follow-up."
        )
    );
}

#[test]
fn build_tool_call_response_flattens_http_error_payloads() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "update_profile",
        "data": {
            "response": {
                "status": 422,
                "body": {
                    "error": "validation_failed",
                    "field": "kind"
                }
            }
        }
    });

    let response = build_tool_call_response("update_profile", envelope, false);
    assert_eq!(response["isError"], json!(true));
    assert_eq!(response["structuredContent"]["status"], "error");
    assert_eq!(
        response["structuredContent"]["error"]["error"],
        "validation_failed"
    );
    assert_eq!(response["structuredContent"]["result"]["field"], "kind");
}

#[test]
fn build_tool_call_response_budgets_public_tools_on_flattened_shape_not_nested_envelope() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let response = build_tool_call_response(
        "read_query",
        json!({
            "status": "complete",
            "phase": "final",
            "tool": "read_query",
            "data": {
                "request": { "path": "/v4/agent/read-query" },
                "response": {
                    "status": 200,
                    "body": {
                        "response_form": "overview_recall",
                        "form_version": 1,
                        "response_family": "overview_recall",
                        "completeness_scope": "complete_as_an_overview_for_the_requested_window_not_as_a_full_raw_dump",
                        "response_kind": "collection",
                        "execution_mode": "retrieval",
                        "items": [{"item_id": "a1"}]
                    }
                },
                "contract": {
                    "continuation": {
                        "status": "continue_on_same_tool",
                        "same_tool": "read_query",
                        "cursor": "rq1:test"
                    },
                    "debug_blob": huge
                }
            }
        }),
        false,
    );

    assert_eq!(response["structuredContent"]["status"], json!("ok"));
    assert_eq!(
        response["structuredContent"]["result"]["items"][0]["item_id"],
        json!("a1")
    );
    assert!(response["structuredContent"].get("truncated").is_none());
}

#[test]
fn payload_limit_truncates_internal_legacy_tools_only() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "kura_projection_get",
        "data": {
            "response": {
                "status": 200,
                "body": {
                    "blob": huge
                }
            }
        }
    });

    let limited = enforce_tool_payload_limit("kura_projection_get", envelope);
    assert_eq!(limited["truncated"], true);
    assert!(
        limited["truncation"]["details_hint"]
            .as_str()
            .unwrap_or_default()
            .contains("Retry with narrower scope or pagination")
    );
    assert!(serialized_json_size_bytes(&limited) <= TOOL_ENVELOPE_MAX_BYTES);
}

#[test]
fn public_read_query_overflow_is_contract_error_without_truncated_payload() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "read_query",
        "data": {
            "response": {
                "status": 200,
                "body": {
                    "items": [{"blob": huge}],
                    "continuation": Value::Null
                }
            }
        }
    });

    let response = build_tool_call_response("read_query", envelope, false);
    let structured = &response["structuredContent"];
    assert_eq!(response["isError"], true);
    assert_eq!(structured["status"], json!("error"));
    assert_eq!(
        structured["error"]["reason_code"],
        json!("payload_contract_overflow")
    );
    assert!(!contains_object_key(structured, "truncated"));
    assert!(
        !response["content"][0]["text"]
            .as_str()
            .unwrap_or_default()
            .contains("truncated")
    );
}

#[test]
fn public_analyze_overflow_preserves_same_surface_continuation_without_truncated_payload() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "analyze",
        "data": {
            "response": {
                "status": 200,
                "body": {
                    "type": "answer",
                    "result_view": {
                        "driver_results": {
                            "items": [{"blob": huge}],
                            "continuation": {
                                "has_more": true,
                                "cursor": "driver-page-2",
                                "next_request": {
                                    "mode": "run",
                                    "canonical_request": {
                                        "question_type": "explanation",
                                        "scope_level": "domain_overview",
                                        "outcome": {"signal": "jump_height"},
                                        "result_page": {
                                            "cursor": "driver-page-2",
                                            "limit": 1
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "agent_usage": {
                        "answer_complete": true
                    }
                }
            }
        }
    });

    let response = build_tool_call_response("analyze", envelope, false);
    let structured = &response["structuredContent"];
    assert_eq!(response["isError"], true);
    assert_eq!(structured["status"], json!("error"));
    assert_eq!(structured["continuation"]["cursor"], json!("driver-page-2"));
    assert_eq!(
        structured["continuation"]["next_request"]["mode"],
        json!("run")
    );
    assert!(
        structured["next_action"]
            .as_str()
            .unwrap_or_default()
            .contains("analyze")
    );
    assert!(!contains_object_key(structured, "truncated"));
}

#[test]
fn payload_limit_for_removed_training_projection_points_to_activity_session_history_recovery() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "kura_projection_get",
        "request": {
            "path": "/v1/projections/authoritative_training_memory/overview",
            "projection_type": "authoritative_training_memory",
            "key": "overview"
        },
        "response": {
            "ok": true,
            "status": 200,
            "body": {
                "blob": huge
            }
        }
    });

    let limited = enforce_tool_payload_limit("kura_projection_get", envelope);
    let hint = limited["truncation"]["details_hint"]
        .as_str()
        .unwrap_or_default();
    assert!(hint.contains("source_id=activity_session_history"));
    assert!(hint.contains("subject=day_history"));
    assert!(hint.contains("subject=session_history"));
    assert!(hint.contains("subject=session_detail"));
    assert!(hint.contains("subject=block_history"));
    assert!(hint.contains("removed legacy training projections"));
}

#[test]
fn payload_limit_for_recovery_points_to_index_and_detail_recovery() {
    let huge = "x".repeat(TOOL_ENVELOPE_MAX_BYTES * 2);
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "kura_projection_get",
        "request": {
            "path": "/v1/projections/recovery/overview",
            "projection_type": "recovery",
            "key": "overview"
        },
        "response": {
            "ok": true,
            "status": 200,
            "body": {
                "blob": huge
            }
        }
    });

    let limited = enforce_tool_payload_limit("kura_projection_get", envelope);
    let hint = limited["truncation"]["details_hint"]
        .as_str()
        .unwrap_or_default();
    assert!(hint.contains("read_query with source_id=recovery"));
    assert!(hint.contains("read_kind=list"));
    assert!(hint.contains("entry_id or date"));
}

#[test]
fn tool_text_content_prefers_truncation_hint() {
    let envelope = json!({
        "status": "complete",
        "phase": "final",
        "tool": "kura_projection_get",
        "truncated": true,
        "truncation": {
            "details_hint": "Retry with narrower scope or pagination."
        }
    });

    let text = tool_text_content("kura_projection_get", &envelope);
    assert!(text.contains("truncated"));
    assert!(text.contains("narrower scope or pagination"));
}

#[test]
fn capability_negotiation_prefers_agent_contract_when_manifest_is_valid() {
    let response = ApiCallResult {
        status: 200,
        body: json!({
            "preferred_startup_endpoint": "/v1/agent/context/section-index",
            "preferred_section_fetch_endpoint": "/v1/agent/context/section-fetch",
            "min_mcp_version": "0.1.0"
        }),
        headers: None,
    };
    let profile = capability_profile_from_negotiation(Ok(response));

    assert_eq!(profile.mode, CapabilityMode::PreferredContract);
    assert_eq!(profile.effective_read_endpoint(), "/v1/agent/context");
    assert_eq!(
        profile.preferred_startup_endpoint,
        "/v1/agent/context/section-index"
    );
    assert_eq!(
        profile.preferred_section_fetch_endpoint,
        "/v1/agent/context/section-fetch"
    );
    assert_eq!(profile.reason, "capabilities_manifest_ok");
}

#[test]
fn capability_profile_public_value_stays_tool_focused() {
    let profile = CapabilityProfile::preferred(
        "/v1/agent/context".to_string(),
        "/v1/agent/context/section-index".to_string(),
        "/v1/agent/context/section-fetch".to_string(),
        json!({
            "preferred_write_endpoint": "/v4/agent/record-activity",
            "maintenance_write_endpoint": "/v4/agent/update-plan"
        }),
        Vec::new(),
    );
    let payload = profile.to_value();
    assert_eq!(
        payload["preferred_startup_endpoint"],
        json!("/v1/agent/context/section-index")
    );
    assert_eq!(
        payload["preferred_section_fetch_endpoint"],
        json!("/v1/agent/context/section-fetch")
    );
    assert_eq!(payload["default_logging_tool"], json!("record_activity"));
    assert_eq!(
        payload["maintenance_surface_visibility"],
        json!("explicit_only")
    );
    assert_eq!(
        payload["public_write_world"],
        json!("task_first_truth_backed")
    );
    assert!(payload.get("preferred_read_endpoint").is_none());
    assert!(payload.get("preferred_write_endpoint").is_none());
    assert!(payload.get("maintenance_write_endpoint").is_none());
    assert!(payload.get("compatibility_write_endpoint").is_none());
    assert_eq!(
        payload["manifest_snapshot"]["public_write_world"],
        json!("task_first_truth_backed")
    );
}

#[test]
fn capability_negotiation_falls_back_for_legacy_server() {
    let response = ApiCallResult {
        status: 404,
        body: json!({
            "error": "not_found"
        }),
        headers: None,
    };
    let profile = capability_profile_from_negotiation(Ok(response));

    assert_eq!(profile.mode, CapabilityMode::LegacyFallback);
    assert_eq!(profile.effective_read_endpoint(), "/v1/projections");
    assert!(profile.reason.starts_with("capability_negotiation_http_"));
}

#[test]
fn capability_negotiation_falls_back_on_min_version_mismatch() {
    let response = ApiCallResult {
        status: 200,
        body: json!({
            "preferred_startup_endpoint": "/v1/agent/context/section-index",
            "preferred_section_fetch_endpoint": "/v1/agent/context/section-fetch",
            "min_mcp_version": "999.0.0"
        }),
        headers: None,
    };
    let profile = capability_profile_from_negotiation(Ok(response));

    assert_eq!(profile.mode, CapabilityMode::LegacyFallback);
    assert_eq!(profile.reason, "capability_negotiation_version_mismatch");
    assert!(
        profile
            .warnings
            .iter()
            .any(|w| w.contains("min_mcp_version"))
    );
}

#[test]
fn write_contract_surface_exposes_user_action_persist_intent_when_present() {
    let body = json!({
        "verification": {"status": "verified"},
        "claim_guard": {"allow_saved_claim": true},
        "save_state": {"status": "saved", "claim_status": "saved_verified", "allow_saved_claim": true},
        "workflow_gate": {"status": "allowed", "phase": "profile_completion"},
        "persist_intent": {
            "mode": "ask_first",
            "status_label": "not_saved",
            "user_prompt": "Confirm before saving.",
            "reason_codes": ["high_impact_write"]
        },
        "post_task_reflection": {"certainty_state": "confirmed", "follow_up_recommended": false},
        "user_notices": [{"kind": "client_update"}]
    });
    let surface = write_contract_surface(&body);
    assert_eq!(surface["verification"]["status"], "verified");
    assert_eq!(surface["claim_guard"]["allow_saved_claim"], true);
    assert_eq!(surface["save_state"]["status"], "saved");
    assert_eq!(surface["workflow_gate"]["status"], "allowed");
    assert_eq!(surface["persist_intent"]["mode"], "ask_first");
    assert_eq!(surface["persist_intent"]["status_label"], "not_saved");
    assert_eq!(
        surface["persist_intent"]["user_prompt"],
        "Confirm before saving."
    );
    assert_eq!(
        surface["post_task_reflection"]["certainty_state"],
        "confirmed"
    );
    assert_eq!(surface["user_notices"][0]["kind"], "client_update");
}

#[test]
fn write_contract_surface_hides_auto_save_persist_intent() {
    let surface = write_contract_surface(&json!({
        "persist_intent": {"mode": "auto_save", "status_label": "saved"}
    }));
    assert_eq!(surface["persist_intent"], Value::Null);
}

#[test]
fn write_contract_surface_defaults_to_null_when_fields_missing() {
    let surface = write_contract_surface(&json!({"unexpected": true}));
    assert_eq!(surface["verification"], Value::Null);
    assert_eq!(surface["claim_guard"], Value::Null);
    assert_eq!(surface["save_state"], Value::Null);
    assert_eq!(surface["workflow_gate"], Value::Null);
    assert_eq!(surface["persist_intent"], Value::Null);
    assert_eq!(surface["post_task_reflection"], Value::Null);
    assert_eq!(surface["user_notices"], Value::Null);
}

#[test]
fn user_notice_relay_builds_appendix_for_must_append_once_notices() {
    let current_version = env!("CARGO_PKG_VERSION");
    let relay = build_user_notice_relay(Some(&json!({
        "user_notices": [
            {
                "notice_id": format!("client_update:kura-cli:{}", current_version),
                "relay_policy": "must_append_once",
                "message_short": format!("Kura CLI update available ({}).", current_version),
                "upgrade_command": "cargo install kura-cli --locked --force"
            }
        ]
    })));
    assert_eq!(relay["status"], "relay_required");
    assert_eq!(relay["must_append_once"], true);
    let appendix = relay["append_to_user_reply"].as_str().unwrap_or_default();
    assert!(appendix.contains("Kura notice:"));
    assert!(appendix.contains("Kura CLI update available"));
    assert!(appendix.contains("cargo install kura-cli --locked --force"));
}

#[test]
fn user_notice_relay_ignores_non_relay_notices() {
    let relay = build_user_notice_relay(Some(&json!({
        "user_notices": [
            {"relay_policy": "advisory_only", "message_short": "ignore"}
        ]
    })));
    assert_eq!(relay["status"], "none");
    assert_eq!(relay["notices"], json!([]));
}

#[test]
fn notice_ack_header_value_uses_seen_notice_ids_for_session() {
    let session_id = "notice-ack-session";
    super::PENDING_NOTICE_ACK_IDS_BY_SESSION
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .remove(session_id);
    queue_pending_notice_ack_ids(session_id, vec!["client_update:kura-mcp:0.1.5".to_string()]);
    let header = pending_notice_ack_header_value(session_id);
    assert_eq!(header.as_deref(), Some("client_update:kura-mcp:0.1.5"));
}

#[test]
fn extract_agent_user_notice_ack_ids_reads_notice_id_field() {
    let ids = extract_agent_user_notice_ack_ids(Some(&json!({
        "user_notices": [
            {"notice_id": "client_update:kura-cli:0.1.5"},
            {"notice_id": "client_update:kura-mcp:0.1.5"}
        ]
    })));
    assert_eq!(
        ids,
        vec![
            "client_update:kura-cli:0.1.5".to_string(),
            "client_update:kura-mcp:0.1.5".to_string()
        ]
    );
}

#[test]
fn kura_api_request_schema_exposes_query_entries_alias_without_schema_combinators() {
    let defs = tool_definitions();
    let api_tool = defs
        .iter()
        .find(|definition| definition.name == "kura_api_request")
        .expect("kura_api_request definition must exist");
    assert_eq!(
        api_tool.input_schema["properties"]["query"]["type"],
        json!("object")
    );
    assert_eq!(
        api_tool.input_schema["properties"]["query_entries"]["type"],
        json!("array")
    );
    assert!(api_tool.input_schema.get("oneOf").is_none());
}

#[test]
fn published_tool_schemas_avoid_unsupported_schema_combinators() {
    for tool in published_tool_definitions() {
        let mut keywords = BTreeSet::new();
        collect_unsupported_schema_keywords(&tool.input_schema, &mut keywords);
        assert!(
            keywords.is_empty(),
            "published schema for {} still contains unsupported keywords: {:?}",
            tool.name,
            keywords
        );
    }
}

#[test]
fn public_tool_capabilities_are_declared_for_read_analyze_and_writes() {
    assert_eq!(
        public_tool_capabilities("read_query")["read_only"],
        json!(true)
    );
    assert_eq!(
        public_tool_capabilities("read_query")["response_surface"],
        json!("facts")
    );
    assert_eq!(
        public_tool_capabilities("analyze")["response_surface"],
        json!("interpretation")
    );
    assert_eq!(
        public_tool_capabilities("record_activity")["side_effect_level"],
        json!("user_state_write")
    );
}

#[test]
fn tool_schema_compatibility_summary_reports_clean_transport_schemas() {
    let summary = tool_schema_compatibility_summary();
    assert_eq!(summary["compatible"], json!(true));
    assert_eq!(summary["incompatible_tools"], json!([]));
}

#[test]
fn legacy_public_surface_no_longer_publishes_removed_training_tool() {
    let defs = published_tool_definitions();
    assert!(
        defs.iter()
            .all(|definition| definition.name != "removed_training_tool")
    );
}

#[test]
fn public_tool_surface_hides_operator_tools_by_default() {
    let names: Vec<&str> = published_tool_definitions()
        .iter()
        .map(|definition| definition.name)
        .collect();
    assert!(!names.contains(&"write_event_legacy"));
    assert!(!names.contains(&"kura_api_request"));
    assert!(!names.contains(&"kura_analysis_job_create"));
    assert!(!names.contains(&"kura_analysis_job_get"));
    assert!(!names.contains(&"kura_semantic_resolve"));
    assert!(names.contains(&"read_query"));
    assert!(names.contains(&"analyze"));
    // Phase-4 cutover: archetype-era analyze_expert is gone.
    assert!(!names.contains(&"analyze_expert"));
    assert!(names.contains(&"record_activity"));
    assert!(names.contains(&"record_recovery"));
    assert!(names.contains(&"report_health"));
    assert!(names.contains(&"record_nutrition"));
    assert!(names.contains(&"record_measurement"));
    assert!(names.contains(&"record_benchmark"));
    assert!(names.contains(&"update_profile"));
    assert!(names.contains(&"plan_activity"));
    assert!(names.contains(&"update_plan"));
    assert!(!names.contains(&"write_event"));
    assert!(!names.contains(&"family_write_turn"));
    assert!(!names.contains(&"write_training"));
    assert!(!names.contains(&"read_training_history"));
}

#[test]
fn public_tool_surface_keeps_operator_tools_removed_after_hard_cut() {
    let names: Vec<&str> = published_tool_definitions()
        .iter()
        .map(|definition| definition.name)
        .collect();
    assert!(!names.contains(&"write_event_legacy"));
    assert!(!names.contains(&"family_write_turn"));
    assert!(!names.contains(&"kura_api_request"));
    assert!(!names.contains(&"kura_analysis_job_create"));
    assert!(!names.contains(&"kura_analysis_job_get"));
    assert!(!names.contains(&"kura_semantic_resolve"));
}

#[test]
fn vnext_public_tool_surface_exposes_new_core_and_hides_legacy_writes() {
    let names: Vec<&str> = published_tool_definitions()
        .iter()
        .map(|definition| definition.name)
        .collect();
    assert!(names.contains(&"read_query"));
    assert!(names.contains(&"record_activity"));
    assert!(names.contains(&"record_recovery"));
    assert!(names.contains(&"report_health"));
    assert!(names.contains(&"record_nutrition"));
    assert!(names.contains(&"record_measurement"));
    assert!(names.contains(&"record_benchmark"));
    assert!(names.contains(&"update_profile"));
    assert!(names.contains(&"plan_activity"));
    assert!(names.contains(&"update_plan"));
    assert!(names.contains(&"analyze"));
    // Phase-4 cutover: archetype-era analyze_expert is gone; only the
    // canonical request-first `analyze` tool survives.
    assert!(!names.contains(&"analyze_expert"));
    assert!(!names.contains(&"analyze_invoke"));
    assert!(!names.contains(&"write_event"));
    assert!(!names.contains(&"kura_agent_brief"));
    assert!(!names.contains(&"kura_agent_context"));
    assert!(!names.contains(&"kura_agent_section_index"));
    assert!(!names.contains(&"kura_agent_section_fetch"));
    assert!(!names.contains(&"kura_analysis_job_create"));
    assert!(!names.contains(&"kura_analysis_job_get"));
    assert!(!names.contains(&"removed_training_tool"));
    assert!(!names.contains(&"family_write_turn"));
    assert!(!names.contains(&"write_event_legacy"));
    assert!(!names.contains(&"write_training"));
    assert!(!names.contains(&"read_training_history"));
}

#[test]
fn vnext_read_query_schema_exposes_universal_public_read_surface() {
    let read_query = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "read_query")
        .expect("read_query definition must exist");

    assert!(
        read_query
            .description
            .contains("Universal canonical read surface")
    );
    assert!(read_query.description.contains("saved sessions"));
    assert_eq!(
        read_query.input_schema["required"],
        json!(["source_id", "read_kind"])
    );
    assert_eq!(
        read_query.input_schema["properties"]["source_id"]["enum"],
        json!([
            "activity_session_history",
            "activity_protocol_profiles",
            "training_plan",
            "training_schedule",
            "recovery",
            "performance_tests",
            "measurements",
            "wearable_daily",
            "evidence_fact_health_log",
            "nutrition",
            "supplements",
            "profile",
            "analyze_catalog"
        ])
    );
    assert_eq!(
        read_query.input_schema["properties"]["read_kind"]["enum"],
        json!(["lookup", "list", "timeline", "aggregate"])
    );
    assert_eq!(
        read_query.input_schema["properties"]["query"]["type"],
        json!("object")
    );
    assert!(
        read_query.input_schema["properties"]["query"]["description"]
            .as_str()
            .expect("query description")
            .contains("bounded programmable read program")
    );
    assert!(
        read_query.input_schema["description"]
            .as_str()
            .expect("schema description")
            .contains("Use analyze first for trend")
    );
    let examples = read_query.input_schema["examples"]
        .as_array()
        .expect("schema examples");
    assert!(
        examples
            .iter()
            .any(|example| example["payload"]["source_id"] == json!("performance_tests"))
    );
    assert!(examples.iter().any(|example| {
        example["payload"]["source_id"] == json!("training_schedule")
            && example["payload"]["read_kind"] == json!("lookup")
            && example["payload"]["filters"]["date"].is_string()
    }));
    assert!(examples.iter().any(|example| {
        example["payload"]["source_id"] == json!("training_schedule")
            && example["payload"]["read_kind"] == json!("timeline")
            && example["payload"]["filters"]["date_from"].is_string()
            && example["payload"]["limit"] == json!(5)
    }));
    assert!(examples.iter().any(|example| {
        example["payload"]["filters"]["capability_target"] == json!("mobility")
    }));
    assert_eq!(
        read_query.input_schema["properties"]["analysis_mode"]["enum"],
        json!(["none", "allow_hybrid", "require_analysis"])
    );
}

/// Spec §4: Discovery lives on `read_query`, not on a separate tool. The
/// published MCP schema must declare `analyze_catalog` as a first-class
/// `source_id`, expose its catalog subjects
/// (candidates/signals/entities/vocabulary_search/signal_detail) in the subject
/// description, and carry at least one example per subject
/// so the agent can find them via tool introspection instead of having to
/// read prose docs. Pinning this at the schema boundary prevents silent
/// drift between read.rs (backend) and the public tool surface. See
/// kura-training-mb97m.
#[test]
fn vnext_read_query_schema_exposes_analyze_catalog_signal_vocabulary_views() {
    let read_query = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "read_query")
        .expect("read_query definition must exist");

    let source_ids = read_query.input_schema["properties"]["source_id"]["enum"]
        .as_array()
        .expect("source_id enum must be an array");
    assert!(
        source_ids
            .iter()
            .any(|value| value == &json!("analyze_catalog")),
        "source_id enum must include analyze_catalog — see spec §4 and kura-training-mb97m"
    );

    let subject_desc = read_query.input_schema["properties"]["subject"]["description"]
        .as_str()
        .expect("subject description must be a string");
    for subject in [
        "candidates",
        "signals",
        "entities",
        "vocabulary_search",
        "signal_detail",
    ] {
        assert!(
            subject_desc.contains(subject),
            "subject description must document analyze_catalog subject `{subject}`"
        );
    }

    let schema_desc = read_query.input_schema["description"]
        .as_str()
        .expect("schema description must be a string");
    assert!(
        schema_desc.contains("analyze_catalog"),
        "read_query top-level description must steer the agent to analyze_catalog \
             as a bounded follow-up path"
    );
    assert!(
        schema_desc.contains("Use analyze first"),
        "read_query top-level description must route analysis intents to analyze first"
    );
    assert!(
        schema_desc.contains("Do not use read_query or analyze_catalog as routine preflight"),
        "read_query top-level description must block routine pre-analyze detours"
    );

    let filters_desc = read_query.input_schema["properties"]["filters"]["description"]
        .as_str()
        .expect("filters description must be a string");
    assert!(
        filters_desc.contains("analyze_catalog"),
        "filters description must document analyze_catalog filter rules \
             (domain, signal_id)"
    );
    assert!(
        read_query.input_schema["properties"]["filters"]["properties"]["domain"]["type"]
            == json!("string"),
        "filters.domain must be declared as string — used by analyze_catalog signals/entities"
    );
    assert!(
        read_query.input_schema["properties"]["filters"]["properties"]["signal_id"]["type"]
            == json!("string"),
        "filters.signal_id must be declared as string — required by analyze_catalog signal_detail"
    );
    assert!(
        read_query.input_schema["properties"]["filters"]["properties"]["signal"]["type"]
            == json!("string"),
        "filters.signal must be declared as string — alias for analyze_catalog entity filtering"
    );
    assert!(
        read_query.input_schema["properties"]["filters"]["properties"]["q"]["type"]
            == json!("string"),
        "filters.q must be declared as string — required by analyze_catalog search subjects"
    );
    assert!(
        read_query.input_schema["properties"]["filters"]["properties"]["candidate_type"]["enum"]
            == json!([
                "signal",
                "source_signal",
                "scoped_signal",
                "entity",
                "comparison_stream",
                "protocol_variant",
                "body_area",
                "supplement",
                "custom_dimension",
                "candidate_space",
                "group",
                "variable_template"
            ]),
        "filters.candidate_type must declare analyze_catalog candidate choices"
    );

    let examples = read_query.input_schema["examples"]
        .as_array()
        .expect("examples must be an array");
    let catalog_subjects: std::collections::BTreeSet<&str> = examples
        .iter()
        .filter(|example| example["payload"]["source_id"] == json!("analyze_catalog"))
        .filter_map(|example| example["payload"]["subject"].as_str())
        .collect();
    for subject in [
        "candidates",
        "signals",
        "entities",
        "vocabulary_search",
        "signal_detail",
    ] {
        assert!(
            catalog_subjects.contains(subject),
            "MCP examples must include an analyze_catalog example with subject={subject}"
        );
    }
}

#[test]
fn vnext_analyze_schema_exposes_typed_envelope_surface() {
    // Phase-4 cutover plus resolver facade: the published `analyze`
    // tool exposes one typed envelope. Semantic resolution happens inside
    // analyze, not through read_query catalog detours.
    let analyze = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "analyze")
        .expect("analyze definition must exist");

    assert!(analyze.description.contains("typed analyze envelope"));
    assert!(analyze.description.contains("POST /v4/agent/analyze/run"));
    assert!(analyze.description.contains("Timeout is terminal"));
    assert!(
        analyze
            .description
            .contains("do not prefetch sources with read_query")
    );
    assert!(
        analyze
            .description
            .contains("Raw user text is allowed only in audit_context.raw_user_text")
    );
    assert!(analyze.description.contains("stop tool use"));
    assert!(!analyze.description.contains("request_resolution"));
    assert!(!analyze.description.contains("plan_running"));
    assert!(!analyze.description.contains("resume_handle"));

    let schema = &analyze.input_schema;
    assert_eq!(schema["type"], json!("object"));
    assert!(schema.get("required").is_none());

    let props = schema["properties"].as_object().expect("properties");
    assert!(props.contains_key("analysis_spec"));
    assert!(props.contains_key("semantic_request"));
    assert!(props.contains_key("canonical_request"));
    assert!(props.contains_key("audit_context"));
    assert!(
        !props.contains_key("request"),
        "typed schema must not expose deleted public request object"
    );
    assert!(
        !props.contains_key("intent"),
        "public MCP analyze schema must not expose direct `intent`"
    );
    assert!(
        !props.contains_key("resume_handle"),
        "typed schema must not expose deleted resume handles"
    );
    assert!(
        !props.contains_key("analysis"),
        "archetype-era `analysis` wrapper must be gone"
    );
    assert!(
        !props.contains_key("kernel"),
        "archetype-era `kernel` branch must be gone"
    );
    assert!(
        !props.contains_key("continuation"),
        "archetype-era `continuation` field must be gone"
    );
    assert!(
        !props.contains_key("next_request"),
        "archetype-era `next_request` field must be gone"
    );

    let first_example = &schema["examples"][0]["payload"];
    assert!(
        first_example.get("analysis_spec").is_some(),
        "first example must show the role-scoped analysis_spec payload"
    );
    assert!(
        first_example["analysis_spec"].get("variables").is_some(),
        "first example must use analysis_spec variables"
    );
    assert!(
        first_example["analysis_spec"].get("slots").is_none(),
        "first example must not teach legacy slots"
    );
    assert!(
        first_example.get("analysis").is_none(),
        "first example must not ship archetype-era `analysis` wrapper"
    );
}

#[test]
fn read_query_vnext_body_defaults_to_kernel_contract() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("activity_session_history")),
        ("read_kind".to_string(), json!("aggregate")),
        ("subject".to_string(), json!("activity_summary")),
        (
            "filters".to_string(),
            json!({"date_from": "2026-04-01", "block_kind": "repetition_sets"}),
        ),
        ("limit".to_string(), json!(12)),
        ("output_shape".to_string(), json!("aggregate")),
        ("analysis_mode".to_string(), json!("allow_hybrid")),
    ]))
    .expect("body");

    assert_eq!(
        body["schema_version"],
        json!(READ_QUERY_REQUEST_SCHEMA_VERSION)
    );
    assert_eq!(body["source_id"], json!("activity_session_history"));
    assert_eq!(body["read_kind"], json!("aggregate"));
    assert_eq!(body["output_shape"], json!("aggregate"));
    assert_eq!(body["analysis_mode"], json!("allow_hybrid"));
}

#[test]
fn read_query_vnext_body_normalizes_benchmark_source_alias() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("benchmarks")),
        ("read_kind".to_string(), json!("timeline")),
        ("subject".to_string(), json!("test_timeline")),
        ("filters".to_string(), json!({"test_type": "cmj"})),
        ("output_shape".to_string(), json!("series")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("performance_tests"));
    assert_eq!(body["filters"]["test_type"], json!("cmj"));
}

#[test]
fn read_query_vnext_body_normalizes_wearable_daily_source_alias() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("wearable_daily_truth")),
        ("read_kind".to_string(), json!("timeline")),
        ("subject".to_string(), json!("metric_timeline")),
        ("filters".to_string(), json!({"metric_key": "steps"})),
        ("output_shape".to_string(), json!("series")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("wearable_daily"));
    assert_eq!(body["filters"]["metric_key"], json!("steps"));
}

#[test]
fn read_query_vnext_body_accepts_analyze_catalog_follow_up() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("analyze_catalog")),
        ("read_kind".to_string(), json!("list")),
        ("subject".to_string(), json!("vocabulary_search")),
        (
            "filters".to_string(),
            json!({"q": "activity_exercises", "candidate_type": "signal"}),
        ),
        ("limit".to_string(), json!(10)),
        ("output_shape".to_string(), json!("collection")),
        ("analysis_mode".to_string(), json!("none")),
    ]))
    .expect("analyze-issued catalog lookup should pass runtime validation");

    assert_eq!(body["source_id"], json!("analyze_catalog"));
    assert_eq!(body["read_kind"], json!("list"));
    assert_eq!(body["subject"], json!("vocabulary_search"));
    assert_eq!(body["filters"]["candidate_type"], json!("signal"));
}

#[test]
fn read_query_vnext_body_accepts_source_specific_non_training_capabilities() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("recovery")),
        ("read_kind".to_string(), json!("timeline")),
        ("subject".to_string(), json!("daily_checkin")),
        ("limit".to_string(), json!(5)),
        ("output_shape".to_string(), json!("series")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("recovery"));
    assert_eq!(body["read_kind"], json!("timeline"));
    assert_eq!(body["subject"], json!("daily_checkin"));
    assert_eq!(body["output_shape"], json!("series"));
}

#[test]
fn read_query_vnext_body_accepts_activity_session_history_capabilities() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("activity_session_history")),
        ("read_kind".to_string(), json!("list")),
        ("subject".to_string(), json!("day_history")),
        (
            "filters".to_string(),
            json!({"source_kind": "manual", "date_from": "2026-04-01", "date_to": "2026-04-08"}),
        ),
        ("limit".to_string(), json!(5)),
        ("output_shape".to_string(), json!("collection")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("activity_session_history"));
    assert_eq!(body["read_kind"], json!("list"));
    assert_eq!(body["filters"]["source_kind"], json!("manual"));
    assert_eq!(body["subject"], json!("day_history"));
    assert_eq!(body["filters"]["date_from"], json!("2026-04-01"));
}

#[test]
fn read_query_vnext_body_accepts_activity_protocol_profile_capabilities() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("activity_protocol_profiles")),
        ("read_kind".to_string(), json!("list")),
        ("subject".to_string(), json!("profile_registry")),
        (
            "filters".to_string(),
            json!({"block_kind": "distance_intervals"}),
        ),
        ("limit".to_string(), json!(5)),
        ("output_shape".to_string(), json!("collection")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("activity_protocol_profiles"));
    assert_eq!(body["read_kind"], json!("list"));
    assert_eq!(body["filters"]["block_kind"], json!("distance_intervals"));
}

#[test]
fn read_query_vnext_body_accepts_bounded_activity_program_block() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("activity_session_history")),
        ("read_kind".to_string(), json!("aggregate")),
        ("subject".to_string(), json!("block_history")),
        (
            "query".to_string(),
            json!({
                "group_by": "comparison_stream",
                "aggregate": "load_kg_max",
                "sort": {"field": "load_kg_max", "direction": "desc"}
            }),
        ),
        ("output_shape".to_string(), json!("aggregate")),
    ]))
    .expect("bounded query block should pass through");

    assert_eq!(body["query"]["group_by"], json!("comparison_stream"));
    assert_eq!(body["query"]["aggregate"], json!("load_kg_max"));
}

#[test]
fn read_query_vnext_body_rejects_removed_legacy_training_source() {
    let error = McpServer::build_read_query_vnext_body(&Map::from_iter([
        (
            "source_id".to_string(),
            json!("authoritative_training_memory"),
        ),
        ("read_kind".to_string(), json!("list")),
    ]))
    .expect_err("legacy training source should fail");

    assert_eq!(error.code, "validation_failed");
    assert_eq!(error.field.as_deref(), Some("source_id"));
    assert!(error.message.contains("removed"));
}

#[test]
fn read_query_vnext_body_rejects_read_kinds_outside_source_capabilities() {
    let error = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("training_plan")),
        ("read_kind".to_string(), json!("timeline")),
    ]))
    .expect_err("unsupported non-training read kind should fail");

    assert_eq!(error.code, "validation_failed");
    assert_eq!(error.field.as_deref(), Some("read_kind"));
}

#[test]
fn read_query_vnext_body_accepts_profile_truth_timeline() {
    let body = McpServer::build_read_query_vnext_body(&Map::from_iter([
        ("source_id".to_string(), json!("profile")),
        ("read_kind".to_string(), json!("timeline")),
        ("subject".to_string(), json!("profile_timeline")),
        (
            "filters".to_string(),
            json!({"write_kind": "goal", "status": "active"}),
        ),
        ("limit".to_string(), json!(5)),
        ("output_shape".to_string(), json!("series")),
    ]))
    .expect("body");

    assert_eq!(body["source_id"], json!("profile"));
    assert_eq!(body["read_kind"], json!("timeline"));
    assert_eq!(body["filters"]["write_kind"], json!("goal"));
}

/// Spec §8 Deletion-Liste: archetype-first MCP schema examples must
/// disappear ersatzlos. The legacy archetype schema-version string (prefix
/// `analysis_archetype_request`, suffix `.v1`) anchors those examples; its
/// presence in this file implies the archetype-first schema shape was not
/// fully removed. The architecture test
/// ``test_458_analyze_phase_4b_deletions_contract`` also pins this absence
/// at the repository boundary; this test pins it at the crate boundary so a
/// local ``cargo test -p kura-mcp-runtime`` catches revivals.
///
/// The forbidden string is rebuilt from parts so this assertion's own
/// source text never contains the concatenated form and therefore never
/// matches itself.
#[test]
fn archetype_first_schema_version_is_absent() {
    let source = include_str!("lib.rs");
    let forbidden = {
        let mut s = String::from("analysis_archetype_request");
        s.push_str(".v1");
        s
    };
    assert!(
        !source.contains(&forbidden),
        "`{forbidden}` must not appear in mcp-runtime/src/lib.rs — spec §8 \
             mandates ersatzlos deletion of the archetype-first schema shape. \
             See kura-training-ywhyd."
    );
}

#[test]
fn read_query_continuation_contract_stays_on_same_tool() {
    let contract = build_read_query_continuation_contract(&json!({
        "continuation": {
            "mode": "cursor",
            "has_more": true,
            "cursor": "rq1:test",
            "returned_count": 2,
            "remaining_count": 3,
            "reason_code": "page_boundary_reached",
            "next_request": {
                "source_id": "activity_session_history",
                "read_kind": "list",
                "cursor": "rq1:test"
            }
        }
    }));

    assert_eq!(contract["status"], json!("continue_on_same_tool"));
    assert_eq!(contract["same_tool"], json!("read_query"));
    assert_eq!(contract["cursor"], json!("rq1:test"));
    assert_eq!(
        contract["next_request"]["source_id"],
        json!("activity_session_history")
    );
}

#[test]
fn read_query_limit_shrinks_for_payload_safe_retry() {
    let mut body = json!({
        "schema_version": READ_QUERY_REQUEST_SCHEMA_VERSION,
        "source_id": "activity_session_history",
        "read_kind": "list",
        "limit": 20
    });

    assert!(McpServer::shrink_read_query_limit(&mut body));
    assert_eq!(body["limit"], json!(10));
    assert!(McpServer::shrink_read_query_limit(&mut body));
    assert_eq!(body["limit"], json!(5));
    assert!(McpServer::shrink_read_query_limit(&mut body));
    assert_eq!(body["limit"], json!(2));
    assert!(McpServer::shrink_read_query_limit(&mut body));
    assert_eq!(body["limit"], json!(1));
    assert!(!McpServer::shrink_read_query_limit(&mut body));
}

#[test]
fn record_activity_vnext_body_accepts_special_action_payloads() {
    let rename = McpServer::build_record_activity_vnext_body(&Map::from_iter([
        ("idempotency_key".to_string(), json!("rename-1")),
        (
            "display_label_action".to_string(),
            json!({
                "action": "rename",
                "display_label": "Spanish Squat",
                "exercise_identity": "spanish_squat_bilateral"
            }),
        ),
    ]))
    .expect("rename special action body");
    assert_eq!(rename["idempotency_key"], json!("rename-1"));
    assert_eq!(rename["display_label_action"]["action"], json!("rename"));

    let clear = McpServer::build_record_activity_vnext_body(&Map::from_iter([
        ("idempotency_key".to_string(), json!("clear-1")),
        (
            "display_label_action".to_string(),
            json!({
                "action": "clear",
                "exercise_identity": "spanish_squat_bilateral"
            }),
        ),
    ]))
    .expect("clear special action body");
    assert_eq!(clear["idempotency_key"], json!("clear-1"));
    assert_eq!(clear["display_label_action"]["action"], json!("clear"));

    let merge_preview = McpServer::build_record_activity_vnext_body(&Map::from_iter([
        ("idempotency_key".to_string(), json!("merge-1")),
        (
            "exercise_identity_merge".to_string(),
            json!({
                "action": "preview",
                "surviving_exercise_identity": "spanish_squat_bilateral",
                "source_exercise_identities": ["spanish_squat_bilateral_iso"]
            }),
        ),
    ]))
    .expect("merge preview body");
    assert_eq!(
        merge_preview["exercise_identity_merge"]["action"],
        json!("preview")
    );

    let continuation = McpServer::build_record_activity_vnext_body(&Map::from_iter([
        ("idempotency_key".to_string(), json!("continuation-1")),
        (
            "continuation".to_string(),
            json!({
                "mode": "activity_identity_merge_approval",
                "clarification_id": "activity_merge_abc123",
                "approve_merge": true
            }),
        ),
    ]))
    .expect("continuation body");
    assert_eq!(continuation["continuation"]["approve_merge"], json!(true));

    let identity_continuation = McpServer::build_record_activity_vnext_body(&Map::from_iter([
        (
            "idempotency_key".to_string(),
            json!("identity-continuation-1"),
        ),
        (
            "continuation".to_string(),
            json!({
                "mode": "activity_identity_clarification",
                "clarification_id": "activity_identity_abc123",
                "confirm_new_exercise": true
            }),
        ),
    ]))
    .expect("identity continuation body");
    assert_eq!(
        identity_continuation["continuation"]["mode"],
        json!("activity_identity_clarification")
    );
    assert_eq!(
        identity_continuation["continuation"]["confirm_new_exercise"],
        json!(true)
    );
}

#[test]
fn vnext_record_activity_schema_exposes_mixed_session_blocks_and_same_surface_repairs() {
    let record_activity = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "record_activity")
        .expect("record_activity definition must exist");

    assert_eq!(
        record_activity.input_schema["properties"]["blocks"]["type"],
        json!("array")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["operation"]["enum"],
        json!(["remove", "replace"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["occurrence_status"]["enum"],
        json!(["executed", "planned"])
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("executed activity"))
    );
    assert_eq!(
        record_activity.input_schema["properties"]["blocks"]["items"]["properties"]["kind"]["enum"],
        json!([
            "repetition_sets",
            "holds",
            "timed_intervals",
            "distance_intervals",
            "continuous_endurance",
            "drill",
            "composite_work",
            "benchmark_result"
        ])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["blocks"]["items"]["properties"]["protocol_profile_id"]
            ["type"],
        json!("string")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["blocks"]["items"]["properties"]["display_label"]
            ["type"],
        json!("string")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["blocks"]["items"]["properties"]["analysis_grade"]
            ["enum"],
        json!(["comparable", "limited", "context_only"])
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("display_label"))
    );
    assert!(
        record_activity
            .description
            .contains("only normal public activity logging tool")
    );
    assert!(
        record_activity
            .description
            .contains("same tool for saved-block corrections")
    );
    assert!(
        record_activity
            .description
            .contains("protocol_profile.action=create|revise")
    );
    assert!(record_activity.description.contains("apply_to_block_id"));
    assert_eq!(
        record_activity.input_schema["properties"]["protocol_profile"]["properties"]["action"]["enum"],
        json!(["create", "revise"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["protocol_profile"]["properties"]["apply_to_block_id"]
            ["type"],
        json!("string")
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("apply_to_block_id"))
    );
    assert_eq!(
        record_activity.input_schema["properties"]["protocol_profile"]["properties"]["block_kind"]
            ["enum"],
        record_activity.input_schema["properties"]["blocks"]["items"]["properties"]["kind"]["enum"]
    );
    assert_eq!(
        record_activity.input_schema["properties"]["protocol_profile"]["required"],
        json!(["action", "profile_label", "block_kind"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["target"]["properties"]["session_id"]["type"],
        json!("string")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["target"]["properties"]["item_id"]["type"],
        json!("string")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["replacement_patch"]["type"],
        json!("object")
    );
    assert_eq!(
        record_activity.input_schema["properties"]["display_label_action"]["properties"]["action"]
            ["enum"],
        json!(["rename", "clear"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["display_label_action"]["required"],
        json!(["action"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["exercise_identity_merge"]["properties"]["action"]
            ["enum"],
        json!(["preview"])
    );
    assert_eq!(
        record_activity.input_schema["properties"]["continuation"]["properties"]["approve_merge"]["type"],
        json!("boolean")
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("display_label_action.action=rename"))
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("action=clear"))
    );
    assert!(
        record_activity.input_schema["description"]
            .as_str()
            .is_some_and(
                |description| description.contains("exercise_identity_merge.action=preview")
            )
    );
}

#[test]
fn vnext_record_recovery_schema_exposes_body_symptom_metric_continuation() {
    let record_recovery = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "record_recovery")
        .expect("record_recovery definition must exist");

    assert_eq!(
        record_recovery.input_schema["properties"]["continuation"]["properties"]["selected_metric_id"]
            ["type"],
        json!("string")
    );
    assert_eq!(
        record_recovery.input_schema["properties"]["continuation"]["properties"]["confirm_new_metric"]
            ["type"],
        json!("boolean")
    );
    assert!(
        record_recovery.input_schema["description"]
            .as_str()
            .is_some_and(|description| description.contains("body_symptoms are canonical"))
    );

    let body = McpServer::build_record_recovery_vnext_body(&Map::from_iter([(
        "continuation".to_string(),
        json!({
            "clarification_id": "recovery_body_symptom_metric_abc123",
            "selected_metric_id": "pain:right_knee"
        }),
    )]))
    .expect("record_recovery continuation body");

    assert_eq!(
        body["continuation"]["selected_metric_id"],
        json!("pain:right_knee")
    );
}

#[test]
fn vnext_plan_activity_schema_exposes_training_shaped_planning_tool() {
    let plan_activity = published_tool_definitions()
        .into_iter()
        .find(|definition| definition.name == "plan_activity")
        .expect("plan_activity definition must exist");

    assert!(
        plan_activity
            .description
            .contains("future or not-yet-executed training")
    );
    assert_eq!(
        plan_activity.input_schema["properties"]["date"]["type"],
        json!("string")
    );
    assert_eq!(
        plan_activity.input_schema["properties"]["blocks"]["items"]["properties"]["kind"]["enum"],
        activity_block_kind_schema()["enum"]
    );
    assert_eq!(plan_activity.input_schema["required"], json!(["date"]));

    let body = McpServer::build_plan_activity_vnext_body(&Map::from_iter([
        ("date".to_string(), json!("2026-04-14")),
        ("session_name".to_string(), json!("Power Clean Technik")),
        (
            "blocks".to_string(),
            json!([{
                "kind": "repetition_sets",
                "label": "Hang Power Clean below knee",
                "exercise_identity": "hang_power_clean",
                "measurements": {"sets": 4, "reps_per_set": 3, "load_kg": 70},
                "execution_context": {"start_position": "below_knee"}
            }]),
        ),
    ]))
    .expect("body");

    assert_eq!(body["date"], json!("2026-04-14"));
    assert_eq!(
        body["blocks"][0]["exercise_identity"],
        json!("hang_power_clean")
    );
}

#[test]
fn auto_generated_intent_handshake_is_deterministic_for_same_input() {
    let temporal_basis = json!({
        "schema_version": "temporal_basis.v1",
        "timezone": "Europe/Berlin",
        "today_local_date": "2026-03-21",
        "context_generated_at": "2026-03-21T07:58:00Z"
    });
    let events = vec![json!({
        "event_type": "set.logged"
    })];

    let first = build_default_intent_handshake(
        &events,
        Some("log trap bar deadlift"),
        temporal_basis.clone(),
    );
    let second =
        build_default_intent_handshake(&events, Some("log trap bar deadlift"), temporal_basis);

    assert_eq!(first, second);
    assert_eq!(first["created_at"], json!("2026-03-21T07:58:00Z"));
    assert!(
        first["handshake_id"]
            .as_str()
            .is_some_and(|value| value.starts_with("mcp-hs-"))
    );
}

#[test]
fn high_impact_confirmation_replay_restores_original_family_write_payload() {
    HIGH_IMPACT_CONFIRMATION_REPLAY_CACHE
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .clear();

    let original_body = json!({
        "events": [{
            "event_type": "set.logged",
            "data": {"exercise": "trap_bar_deadlift"}
        }],
        "read_after_write_targets": [{
            "projection_type": "training_log",
            "key": "session:2026-03-20"
        }],
        "intent_handshake": {
            "schema_version": "intent_handshake.v1",
            "goal": "log trap bar deadlift safely",
            "planned_action": "write events: set.logged",
            "assumptions": ["context and request intent are current"],
            "non_goals": ["no unrelated writes outside current task scope"],
            "impact_class": "high_impact_write",
            "success_criteria": "family_write returns verification and claim_guard for this action",
            "created_at": "2026-03-21T07:58:00Z",
            "handshake_id": "mcp-hs-original",
            "temporal_basis": {
                "schema_version": "temporal_basis.v1",
                "timezone": "Europe/Berlin",
                "today_local_date": "2026-03-21",
                "context_generated_at": "2026-03-21T07:58:00Z"
            }
        }
    });
    remember_high_impact_confirmation_replay(
        "/v4/agent/write-event",
        &original_body,
        &json!({
            "received": {
                "confirmation_token": "confirm-123"
            }
        }),
    );

    let (path, replayed_body) = replay_body_for_high_impact_confirmation(
        "confirm-123",
        &json!({
            "schema_version": "high_impact_confirmation.v1",
            "confirmed": true,
            "confirmed_at": "2026-03-21T07:59:30Z",
            "confirmation_token": "confirm-123"
        }),
    )
    .expect("confirmation retry should reuse cached payload");

    assert_eq!(path, "/v4/agent/write-event");
    assert_eq!(replayed_body["events"], original_body["events"]);
    assert_eq!(
        replayed_body["intent_handshake"],
        original_body["intent_handshake"]
    );
    assert_eq!(
        replayed_body["high_impact_confirmation"]["confirmation_token"],
        json!("confirm-123")
    );
}

#[test]
fn canonical_logging_write_detection_marks_core_training_surfaces() {
    let events = vec![
        json!({"event_type": "set.logged"}),
        json!({"event_type": "performance_test.logged"}),
        json!({"event_type": "session.completed"}),
    ];
    assert!(contains_canonical_logging_writes(&events));
    assert!(is_canonical_logging_event_type("set.logged"));
    assert!(!is_canonical_logging_event_type("energy.logged"));
}

#[test]
fn core_repair_write_detection_marks_set_corrections_and_retractions() {
    let events = vec![
        json!({"event_type": "set.corrected"}),
        json!({"event_type": "event.retracted"}),
    ];
    assert!(contains_core_repair_writes(&events));
    assert!(is_core_repair_event_type("set.corrected"));
    assert!(is_core_repair_event_type("event.retracted"));
    assert!(!is_core_repair_event_type("preference.set"));
}

#[test]
fn capability_profile_staleness_uses_ttl_window() {
    let mut profile = CapabilityProfile::preferred(
        "/v1/agent/context".to_string(),
        "/v1/agent/context/section-index".to_string(),
        "/v1/agent/context/section-fetch".to_string(),
        json!({
            "preferred_write_endpoint": "/v4/agent/record-activity",
            "maintenance_write_endpoint": "/v4/agent/update-plan"
        }),
        Vec::new(),
    );
    profile.negotiated_at = chrono::Utc::now() - chrono::Duration::seconds(600);
    assert!(McpServer::capability_profile_is_stale(&profile));

    profile.negotiated_at = chrono::Utc::now();
    assert!(!McpServer::capability_profile_is_stale(&profile));
}

#[test]
fn high_impact_classification_keeps_routine_plan_update_low_impact() {
    let events = vec![json!({
        "event_type": "training_plan.updated",
        "data": {
            "change_scope": "routine_adjustment",
            "volume_delta_pct": 8.0,
            "intensity_delta_pct": 4.0,
            "frequency_delta_per_week": 1.0
        }
    })];
    assert!(!has_high_impact_events(&events));
}

#[test]
fn high_impact_classification_escalates_large_plan_shift() {
    let events = vec![json!({
        "event_type": "training_plan.updated",
        "data": {
            "change_scope": "full_rewrite",
            "replace_entire_plan": true,
            "volume_delta_pct": 22.0
        }
    })];
    assert!(has_high_impact_events(&events));
}

#[test]
fn high_impact_classification_keeps_bounded_schedule_exception_low_impact() {
    let events = vec![json!({
        "event_type": "training_schedule.exception.upsert",
        "data": {
            "exception_id": "deload-start-today",
            "operation": "patch",
            "selector": {
                "local_date": "2026-03-07",
                "session_name": "Technik + Power"
            },
            "progression_override": {
                "deload_active": true,
                "phase": "deload",
                "volume_delta_pct": -30
            }
        }
    })];
    assert!(!has_high_impact_events(&events));
}

#[test]
fn high_impact_classification_escalates_unbounded_schedule_exception() {
    let events = vec![json!({
        "event_type": "training_schedule.exception.upsert",
        "data": {
            "exception_id": "rewrite-future-saturdays",
            "operation": "patch",
            "selector": {
                "session_name": "Technik + Power"
            },
            "rewrite_template": true
        }
    })];
    assert!(has_high_impact_events(&events));
}

#[test]
fn plan_write_detection_matches_training_plan_prefix() {
    let events = vec![
        json!({"event_type": "set.logged", "data": {"reps": 5}}),
        json!({"event_type": "training_plan.updated", "data": {"change_scope": "routine_adjustment"}}),
    ];
    assert!(contains_plan_or_schedule_writes(&events));
}

#[test]
fn plan_write_detection_matches_training_schedule_exception_prefix() {
    let events = vec![
        json!({"event_type": "metric.logged", "data": {"value": 1}}),
        json!({"event_type": "training_schedule.exception.upsert", "data": {"action": "move"}}),
    ];
    assert!(contains_plan_or_schedule_writes(&events));
}

#[test]
fn plan_write_contract_error_exposes_required_mode_and_hint() {
    let err = plan_write_requires_family_write_error(WriteMode::Commit, "preferred_contract", None);
    assert_eq!(err.code, "plan_write_requires_family_write");
    assert_eq!(err.field.as_deref(), Some("mode"));
    assert!(
        err.docs_hint
            .as_deref()
            .unwrap_or_default()
            .contains("family_write")
    );
}

#[test]
fn write_api_error_classification_maps_preflight_blockers() {
    let response = ApiCallResult {
        status: 400,
        body: json!({
            "error": "validation_failed",
            "message": "family_write blocked by preflight checks",
            "docs_hint": "Resolve blockers first",
            "received": {
                "blockers": [
                    {"code": "health_consent_required"}
                ]
            }
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(classification.code, "write_preflight_blocked");
    assert!(classification.message.contains("health_consent_required"));
}

#[test]
fn write_api_error_classification_maps_approval_timeout() {
    let response = ApiCallResult {
        status: 403,
        body: json!({
            "error": "forbidden",
            "message": "exec denied: approval timed out",
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(classification.code, "approval_timeout");
    assert!(classification.message.contains("approval"));
}

#[test]
fn write_api_error_classification_maps_unknown_field_high_impact_block_to_action_contract() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": UNKNOWN_FIELD_HIGH_IMPACT_API_ERROR,
            "message": "High-impact write 'training_plan.created' includes unknown fields without explicit opt-in: phase",
            "docs_hint": "Either use canonical data fields from event_conventions, or include metadata.context.unknown_field_opt_in.",
            "next_action": UNKNOWN_FIELD_HIGH_IMPACT_NEXT_ACTION,
            "next_action_url": "/v1/system/config/section?section=system_config.event_conventions::training_plan.created",
            "received": {
                "unknown_fields": ["phase"],
                "remediation": {
                    "fallback_action": UNKNOWN_FIELD_HIGH_IMPACT_FALLBACK_ACTION
                }
            }
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(classification.code, "unknown_field_high_impact_blocked");
    assert!(classification.message.contains("High-impact write blocked"));
    assert_eq!(
        classification.next_action.as_deref(),
        Some(UNKNOWN_FIELD_HIGH_IMPACT_NEXT_ACTION)
    );
    assert_eq!(
        classification.next_action_url.as_deref(),
        Some(
            "/v1/system/config/section?section=system_config.event_conventions::training_plan.created"
        )
    );
}

#[test]
fn write_api_error_classification_maps_canonical_logging_direct_write_block() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": CANONICAL_LOGGING_DIRECT_WRITE_API_ERROR,
            "message": "Canonical logging events (set.logged) require task-first public write surfaces; raw /v1/events is blocked.",
            "next_action": CANONICAL_LOGGING_DIRECT_WRITE_NEXT_ACTION,
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(
        classification.code,
        CANONICAL_LOGGING_DIRECT_WRITE_API_ERROR
    );
    assert_eq!(
        classification.next_action.as_deref(),
        Some(CANONICAL_LOGGING_DIRECT_WRITE_NEXT_ACTION)
    );
}

#[test]
fn write_api_error_classification_maps_canonical_logging_unknown_field_block() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": CANONICAL_LOGGING_UNKNOWN_FIELD_API_ERROR,
            "message": "Canonical logging event 'set.logged' contains unknown fields: sets",
            "next_action": CANONICAL_LOGGING_UNKNOWN_FIELD_NEXT_ACTION,
            "received": {
                "remediation_v1": {
                    "example_patch": {
                        "events": [
                            {"event_type": "set.logged"}
                        ]
                    }
                }
            }
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(
        classification.code,
        CANONICAL_LOGGING_UNKNOWN_FIELD_API_ERROR
    );
    assert_eq!(
        classification.next_action.as_deref(),
        Some(CANONICAL_LOGGING_UNKNOWN_FIELD_NEXT_ACTION)
    );
    assert_eq!(
        classification.docs_hint.as_deref(),
        Some(
            "Reuse error.details.response.received.remediation_v1.example_patch as the retry template; do not invent new fields or fall back to raw commit."
        )
    );
}

#[test]
fn write_api_error_classification_extracts_remediation_v1_fields() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": SET_TYPE_EXPLICIT_MENTION_API_ERROR,
            "message": "set.logged contains explicit 'amrap' mention, but data.set_type is missing",
            "received": {
                "remediation_v1": {
                    "schema_version": "write_remediation.v1",
                    "code": SET_TYPE_EXPLICIT_MENTION_API_ERROR,
                    "event_type": "set.logged",
                    "field_path": "data.set_type",
                    "expected": {"set_type": {"required": true}},
                    "received": {"mentioned_set_type": "amrap"},
                    "fix_hint": "Persist set_type.",
                    "example_patch": {"event_type": "set.logged", "data": {"set_type": "amrap"}},
                    "retryable": true
                }
            }
        }),
        headers: None,
    };

    let classification = classify_write_api_error(&response);
    assert_eq!(
        classification.remediation_code.as_deref(),
        Some(SET_TYPE_EXPLICIT_MENTION_API_ERROR)
    );
    assert_eq!(
        classification.remediation_event_type.as_deref(),
        Some("set.logged")
    );
    assert_eq!(
        classification.remediation_field_path.as_deref(),
        Some("data.set_type")
    );
    assert_eq!(
        classification.next_action.as_deref(),
        Some(SET_TYPE_EXPLICIT_MENTION_NEXT_ACTION)
    );
}

#[test]
fn derive_write_auto_retry_plan_applies_set_type_patch_from_remediation_contract() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": SET_TYPE_EXPLICIT_MENTION_API_ERROR,
            "field": "data.set_type",
            "received": {
                "remediation_v1": {
                    "received": {"mentioned_set_type": "AMRAP"}
                }
            }
        }),
        headers: None,
    };
    let events = vec![json!({
        "event_type": "set.logged",
        "data": {"exercise_id": "back_extension", "reps": 8, "notes": "AMRAP"},
        "metadata": {"idempotency_key": "idem-1"}
    })];

    let plan = derive_write_auto_retry_plan(&response, &events).expect("auto-retry plan");
    assert_eq!(plan.event_index, 0);
    assert_eq!(plan.reason_code, SET_TYPE_EXPLICIT_MENTION_API_ERROR);
    assert_eq!(plan.applied_fix["field_path"], "data.set_type");
    assert_eq!(plan.patched_events[0]["data"]["set_type"], "amrap");
}

#[test]
fn derive_write_auto_retry_plan_uses_batch_field_index() {
    let response = ApiCallResult {
        status: 422,
        body: json!({
            "error": SET_TYPE_EXPLICIT_MENTION_API_ERROR,
            "field": "events[1].data.set_type",
            "received": {
                "remediation_v1": {
                    "received": {"mentioned_set_type": "warm-up"}
                }
            }
        }),
        headers: None,
    };
    let events = vec![
        json!({
            "event_type": "energy.logged",
            "data": {"level": 7},
            "metadata": {"idempotency_key": "idem-0"}
        }),
        json!({
            "event_type": "set.logged",
            "data": {"exercise_id": "bench_press", "reps": 5, "notes": "warm-up"},
            "metadata": {"idempotency_key": "idem-1"}
        }),
    ];

    let plan = derive_write_auto_retry_plan(&response, &events).expect("auto-retry plan");
    assert_eq!(plan.event_index, 1);
    assert_eq!(plan.patched_events[1]["data"]["set_type"], "warmup");
    assert!(plan.patched_events[0]["data"].get("set_type").is_none());
}

#[test]
fn build_retry_request_for_plan_preserves_family_write_envelope() {
    let plan = WriteAutoRetryPlan {
        reason_code: SET_TYPE_EXPLICIT_MENTION_API_ERROR.to_string(),
        event_index: 0,
        patched_events: vec![json!({
            "event_type": "set.logged",
            "data": {"exercise_id": "back_extension", "reps": 8, "set_type": "amrap"},
            "metadata": {"idempotency_key": "idem-1"}
        })],
        applied_fix: json!({"field_path": "data.set_type", "set_type": "amrap"}),
    };
    let request_body = json!({
        "events": [{
            "event_type": "set.logged",
            "data": {"exercise_id": "back_extension", "reps": 8},
            "metadata": {"idempotency_key": "idem-1"}
        }],
        "read_after_write_targets": [{"projection_type": "quality_health", "key": "overview"}]
    });

    let (path, retry_body) = build_retry_request_for_plan(
        WriteMode::FamilyWrite,
        "/v4/agent/write-event",
        &request_body,
        &plan,
    )
    .expect("retry request");
    assert_eq!(path, "/v4/agent/write-event");
    assert_eq!(retry_body["events"][0]["data"]["set_type"], "amrap");
    assert_eq!(
        retry_body["read_after_write_targets"][0]["projection_type"],
        "quality_health"
    );
}

#[test]
fn tool_error_to_value_includes_next_action_fields_when_present() {
    let payload = ToolError::new("policy_violation", "write blocked")
        .with_next_action("retry_with_unknown_field_opt_in_or_canonical_fields")
        .with_next_action_url("/v1/system/config/manifest")
        .to_value();

    assert_eq!(
        payload.get("next_action").and_then(Value::as_str),
        Some("retry_with_unknown_field_opt_in_or_canonical_fields")
    );
    assert_eq!(
        payload.get("next_action_url").and_then(Value::as_str),
        Some("/v1/system/config/manifest")
    );
}

#[test]
fn context_session_mark_and_check_roundtrip() {
    let sid = format!("test-roundtrip-{}", Uuid::now_v7());
    assert!(!is_context_loaded(&sid));
    mark_context_loaded(&sid);
    assert!(is_context_loaded(&sid));
}

#[test]
fn context_sessions_are_isolated_between_ids() {
    let sid_a = format!("test-iso-a-{}", Uuid::now_v7());
    let sid_b = format!("test-iso-b-{}", Uuid::now_v7());
    mark_context_loaded(&sid_a);
    assert!(is_context_loaded(&sid_a));
    assert!(!is_context_loaded(&sid_b));
}

#[test]
fn session_id_is_credential_derived_for_explicit_token() {
    let server = McpServer::new(McpRuntimeConfig {
        api_url: "http://127.0.0.1:9".to_string(),
        no_auth: true,
        explicit_token: Some("my-secret-token".to_string()),
        allow_admin: false,
    });
    assert!(
        server.session_id.starts_with("sid-"),
        "credential-derived session_id should start with 'sid-', got: {}",
        server.session_id
    );
    assert!(
        !server.session_id.starts_with("tok-"),
        "session_id must not look like an access token handle"
    );
}

#[test]
fn session_id_is_stable_for_same_token() {
    let make = || {
        McpServer::new(McpRuntimeConfig {
            api_url: "http://127.0.0.1:9".to_string(),
            no_auth: true,
            explicit_token: Some("stable-token-test".to_string()),
            allow_admin: false,
        })
    };
    let a = make();
    let b = make();
    assert_eq!(
        a.session_id, b.session_id,
        "same token must produce the same session_id (HTTP mode stability)"
    );
}

#[test]
fn session_id_is_unique_uuid_for_stdio() {
    let make = || {
        McpServer::new(McpRuntimeConfig {
            api_url: "http://127.0.0.1:9".to_string(),
            no_auth: true,
            explicit_token: None,
            allow_admin: false,
        })
    };
    let a = make();
    let b = make();
    assert!(
        a.session_id.starts_with("stdio-"),
        "stdio session_id should start with 'stdio-', got: {}",
        a.session_id
    );
    assert_ne!(
        a.session_id, b.session_id,
        "each stdio server should get a unique session_id"
    );
}

#[test]
fn retrieval_fsm_policy_parser_uses_defaults_when_unset() {
    let policy = parse_retrieval_fsm_policy_from_raw(None, None, None);
    assert_eq!(policy.window_secs, RETRIEVAL_FSM_WINDOW_SECS);
    assert_eq!(
        policy.max_reloads_per_window,
        RETRIEVAL_FSM_MAX_RELOADS_PER_WINDOW
    );
    assert_eq!(
        policy.max_repeat_signature_streak,
        RETRIEVAL_FSM_MAX_REPEAT_SIGNATURE_STREAK
    );
    assert!(!policy.configured_via_env);
}

#[test]
fn retrieval_fsm_policy_parser_accepts_env_overrides() {
    let policy = parse_retrieval_fsm_policy_from_raw(
        Some("120".to_string()),
        Some("40".to_string()),
        Some("5".to_string()),
    );
    assert_eq!(policy.window_secs, 120);
    assert_eq!(policy.max_reloads_per_window, 40);
    assert_eq!(policy.max_repeat_signature_streak, 5);
    assert!(policy.configured_via_env);
}

#[test]
fn retrieval_fsm_policy_parser_clamps_out_of_range_values() {
    let policy = parse_retrieval_fsm_policy_from_raw(
        Some("99999".to_string()),
        Some("0".to_string()),
        Some("-3".to_string()),
    );
    assert_eq!(policy.window_secs, RETRIEVAL_FSM_WINDOW_SECS_MAX);
    assert_eq!(
        policy.max_reloads_per_window,
        RETRIEVAL_FSM_MAX_RELOADS_PER_WINDOW_MIN
    );
    // negative numbers fail parsing and therefore fall back to default.
    assert_eq!(
        policy.max_repeat_signature_streak,
        RETRIEVAL_FSM_MAX_REPEAT_SIGNATURE_STREAK
    );
    assert!(policy.configured_via_env);
}

#[test]
fn retrieval_fsm_blocks_repeated_signature_loops() {
    let sid = format!("test-retrieval-repeat-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();
    let args = json_to_map(json!({
        "projection_type": "exercise_progression",
        "limit": 50,
        "cursor": "abc"
    }));

    for _ in 0..policy.max_repeat_signature_streak {
        observe_tool_call_start(&sid, "kura_projection_list", true);
        assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());
    }

    observe_tool_call_start(&sid, "kura_projection_list", true);
    let blocked = maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy)
        .expect("loop guard should block repeated retrieval signature");
    assert_eq!(blocked.reason_code, "repeated_reload_signature");

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["fsm"]["stop_reason"], "repeated_reload_signature");
    assert_eq!(
        snapshot["metrics"]["abort_reasons"]["repeated_reload_signature"],
        1
    );
}

#[test]
fn retrieval_fsm_blocks_when_max_reload_budget_is_exhausted() {
    let sid = format!("test-retrieval-max-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    for idx in 0..policy.max_reloads_per_window {
        let args = json_to_map(json!({
            "projection_type": "exercise_progression",
            "limit": 50,
            "cursor": format!("cursor-{idx}")
        }));
        observe_tool_call_start(&sid, "kura_projection_list", true);
        assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());
    }

    let overflow_args = json_to_map(json!({
        "projection_type": "exercise_progression",
        "limit": 50,
        "cursor": "cursor-overflow"
    }));
    observe_tool_call_start(&sid, "kura_projection_list", true);
    let blocked = maybe_block_retrieval_loop(&sid, "kura_projection_list", &overflow_args, &policy)
        .expect("loop guard should enforce max reload cap");
    assert_eq!(blocked.reason_code, "max_reloads_exceeded");
}

#[test]
fn retrieval_fsm_resets_after_non_retrieval_step() {
    let sid = format!("test-retrieval-reset-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    let args = json_to_map(json!({
        "projection_type": "exercise_progression",
        "limit": 50
    }));
    observe_tool_call_start(&sid, "kura_projection_list", true);
    assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());

    observe_tool_call_start(&sid, "report_health", true);
    observe_tool_call_start(&sid, "kura_projection_list", true);
    assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["fsm"]["reload_count_in_window"], 1);
    assert_eq!(snapshot["fsm"]["current_reload_depth"], 1);
    assert!(snapshot["fsm"]["stop_reason"].is_null());
}

#[test]
fn retrieval_observability_tracks_overflow_and_abort_reasons() {
    let sid = format!("test-retrieval-observe-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    mark_context_loaded(&sid);
    observe_tool_call_start(&sid, "read_query", true);
    observe_tool_outcome(
        &sid,
        "read_query",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "continuation": { "has_more": false }
                    }
                }
            }
        }),
    );
    observe_tool_call_start(&sid, "kura_projection_list", true);
    observe_tool_outcome(
        &sid,
        "kura_projection_list",
        &json!({
            "data": {
                "request": { "path": "/v1/projections/exercise_progression/paged" }
            }
        }),
    );
    observe_tool_call_start(&sid, "kura_projection_get", true);
    record_abort_reason_for_session(&sid, "context_required_before_write");

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["metrics"]["context_loaded_calls"], 3);
    assert_eq!(snapshot["metrics"]["context_hit_rate"], 1.0);
    assert_eq!(snapshot["metrics"]["projection_page_calls"], 1);
    assert_eq!(
        snapshot["metrics"]["abort_reasons"]["context_required_before_write"],
        1
    );
    assert_eq!(snapshot["metrics"]["speculative_answer_rate"], 0.0);
}

#[test]
fn retrieval_observability_marks_read_query_before_first_analyze() {
    let sid = format!("test-analysis-entry-detour-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    observe_tool_call_start(&sid, "read_query", true);
    observe_tool_call_start(&sid, "read_query", true);
    observe_tool_call_start(&sid, "analyze", true);
    observe_tool_call_start(&sid, "read_query", true);

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["metrics"]["analyze_calls"], json!(1));
    assert_eq!(
        snapshot["metrics"]["read_query_before_first_analyze_count"],
        json!(2)
    );
    assert_eq!(
        snapshot["metrics"]["analysis_entry_detour_detected"],
        json!(true)
    );
    assert!(
        snapshot["metrics"]["abort_reasons"]
            .as_object()
            .expect("abort reasons object")
            .is_empty(),
        "entry detour metric is an evaluation signal, not a runtime blocker"
    );
}

#[test]
fn pending_analyze_retry_blocks_read_query_until_next_analyze_response() {
    let sid = format!("test-pending-analyze-retry-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    observe_tool_outcome(
        &sid,
        "analyze",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "type": "refusal",
                        "next_request": {
                            "action": "retry_analyze",
                            "must_call_before_read_query": true,
                            "reason": "empty_entity_scope_valid_candidate_available",
                            "intent": {
                                "question_type": "trend",
                                "entity": {"entity_id": "hang_muscle_snatch_bk"},
                                "outcome": {"signal": "estimated_1rm"}
                            }
                        }
                    }
                }
            }
        }),
    );

    let block = maybe_block_pending_analyze_retry(&sid, "read_query")
        .expect("read_query must be blocked while analyze retry is pending");
    assert_eq!(block["reason_code"], json!("analyze_retry_required"));
    assert_eq!(
        block["next_request"]["intent"]["entity"]["entity_id"],
        json!("hang_muscle_snatch_bk")
    );
    assert!(maybe_block_pending_analyze_retry(&sid, "analyze").is_none());
    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(
        snapshot["metrics"]["pending_analyze_retry_required"],
        json!(true)
    );
    assert_eq!(
        snapshot["metrics"]["abort_reasons"]["analyze_retry_required"],
        json!(1)
    );

    observe_tool_outcome(
        &sid,
        "analyze",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "type": "answer",
                        "agent_usage": {"answer_complete": true}
                    }
                }
            }
        }),
    );

    assert!(maybe_block_pending_analyze_retry(&sid, "read_query").is_none());
    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(
        snapshot["metrics"]["pending_analyze_retry_required"],
        json!(false)
    );
}

#[test]
fn speculative_answer_rate_tracks_grounded_write_evidence() {
    let sid = format!("test-speculative-rate-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    observe_tool_call_start(&sid, "record_activity", true);
    observe_tool_outcome(
        &sid,
        "record_activity",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "post_task_reflection": {"certainty_state": "partial"}
                    }
                }
            }
        }),
    );
    observe_tool_call_start(&sid, "record_activity", true);
    observe_tool_outcome(
        &sid,
        "record_activity",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "post_task_reflection": {"certainty_state": "confirmed"}
                    }
                }
            }
        }),
    );

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["metrics"]["total_tool_calls"], 2);
    assert_eq!(snapshot["metrics"]["speculative_answer_sample_count"], 2);
    assert_eq!(snapshot["metrics"]["speculative_answer_evidence_count"], 1);
    assert_eq!(snapshot["metrics"]["speculative_answer_signal_count"], 1);
    assert_eq!(snapshot["metrics"]["speculative_answer_rate"], 0.5);
}

#[test]
fn speculative_answer_rate_tracks_activity_saved_phrase_guard() {
    let sid = format!("test-speculative-rate-log-turn-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    observe_tool_call_start(&sid, "record_activity", true);
    observe_tool_outcome(
        &sid,
        "record_activity",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "save_state": {"allows_definitive_saved_phrase": false}
                    }
                }
            }
        }),
    );
    observe_tool_call_start(&sid, "record_activity", true);
    observe_tool_outcome(
        &sid,
        "record_activity",
        &json!({
            "data": {
                "response": {
                    "body": {
                        "save_state": {"allows_definitive_saved_phrase": true}
                    }
                }
            }
        }),
    );

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["metrics"]["total_tool_calls"], 2);
    assert_eq!(snapshot["metrics"]["speculative_answer_sample_count"], 2);
    assert_eq!(snapshot["metrics"]["speculative_answer_evidence_count"], 1);
    assert_eq!(snapshot["metrics"]["speculative_answer_signal_count"], 1);
    assert_eq!(snapshot["metrics"]["speculative_answer_rate"], 0.5);
}

#[test]
fn speculative_answer_rate_ignores_precondition_block_proxy_signals() {
    let sid = format!("test-speculative-rate-blocks-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    observe_tool_call_start(&sid, "kura_projection_get", false);
    record_abort_reason_for_session(&sid, "startup_context_required");
    observe_tool_call_start(&sid, "kura_events_list", false);
    record_abort_reason_for_session(&sid, "context_required_before_write");

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["metrics"]["total_tool_calls"], 2);
    assert_eq!(snapshot["metrics"]["speculative_answer_sample_count"], 0);
    assert_eq!(snapshot["metrics"]["speculative_answer_evidence_count"], 0);
    assert_eq!(snapshot["metrics"]["speculative_answer_rate"], 0.0);
}

#[test]
fn retrieval_replay_contract_allows_progressive_reload_then_resets() {
    let sid = format!("test-retrieval-replay-ok-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    let read_query_args = json_to_map(json!({
        "source_id": "performance_tests",
        "read_kind": "list",
        "limit": 25
    }));
    observe_tool_call_start(&sid, "read_query", true);
    assert!(maybe_block_retrieval_loop(&sid, "read_query", &read_query_args, &policy).is_none());

    for cursor in [None, Some("c1"), Some("c2")] {
        let mut args = json_to_map(json!({
            "projection_type": "exercise_progression",
            "limit": 50
        }));
        if let Some(value) = cursor {
            args.insert("cursor".to_string(), json!(value));
        }
        observe_tool_call_start(&sid, "kura_projection_list", true);
        assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());
    }

    observe_tool_call_start(&sid, "report_health", true);
    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["fsm"]["reload_count_in_window"], 0);
    assert_eq!(snapshot["fsm"]["current_reload_depth"], 0);
    assert!(snapshot["fsm"]["stop_reason"].is_null());
}

#[test]
fn retrieval_replay_contract_stops_cursor_loop_with_reason() {
    let sid = format!("test-retrieval-replay-loop-{}", Uuid::now_v7());
    clear_retrieval_state(&sid);
    let policy = RetrievalFsmPolicy::defaults();

    let args = json_to_map(json!({
        "projection_type": "exercise_progression",
        "limit": 50,
        "cursor": "same-cursor"
    }));

    for _ in 0..policy.max_repeat_signature_streak {
        observe_tool_call_start(&sid, "kura_projection_list", true);
        assert!(maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy).is_none());
    }
    observe_tool_call_start(&sid, "kura_projection_list", true);
    let blocked = maybe_block_retrieval_loop(&sid, "kura_projection_list", &args, &policy)
        .expect("loop replay should terminate with repeated signature stop reason");
    assert_eq!(blocked.reason_code, "repeated_reload_signature");

    let snapshot = retrieval_observability_snapshot(&sid, &policy);
    assert_eq!(snapshot["fsm"]["stop_reason"], "repeated_reload_signature");
    assert_eq!(
        snapshot["metrics"]["abort_reasons"]["repeated_reload_signature"],
        1
    );
}

#[test]
fn determinism_rollout_policy_parser_uses_defaults_when_unset() {
    let policy = parse_determinism_rollout_policy_from_raw(
        None, None, None, None, None, None, None, None, None, None, None, None,
    );
    assert_eq!(policy.mode, DeterminismRolloutMode::Full);
    assert_eq!(policy.canary_percent, DETERMINISM_DEFAULT_CANARY_PERCENT);
    assert!(!policy.abort_on_breach);
    assert_eq!(
        policy.min_context_calls,
        DETERMINISM_DEFAULT_MIN_CONTEXT_CALLS
    );
    assert_eq!(
        policy.min_total_tool_calls,
        DETERMINISM_DEFAULT_MIN_TOTAL_CALLS
    );
    assert_eq!(
        policy.thresholds.overflow_rate_max,
        DETERMINISM_DEFAULT_ABORT_OVERFLOW_RATE_MAX
    );
    assert!(!policy.configured_via_env);
}

#[test]
fn determinism_rollout_policy_parser_accepts_env_overrides() {
    let policy = parse_determinism_rollout_policy_from_raw(
        Some("canary".to_string()),
        Some("25".to_string()),
        Some("sid-a,sid-b".to_string()),
        None,
        None,
        Some("true".to_string()),
        Some("0.35".to_string()),
        Some("0.1".to_string()),
        Some("0.2".to_string()),
        Some("0.0".to_string()),
        Some("5".to_string()),
        Some("15".to_string()),
    );
    assert_eq!(policy.mode, DeterminismRolloutMode::Canary);
    assert_eq!(policy.canary_percent, 25);
    assert!(policy.canary_allowlist.contains("sid-a"));
    assert!(policy.canary_allowlist.contains("sid-b"));
    assert!(policy.abort_on_breach);
    assert_eq!(policy.min_context_calls, 5);
    assert_eq!(policy.min_total_tool_calls, 15);
    assert_eq!(policy.thresholds.overflow_rate_max, 0.35);
    assert_eq!(policy.thresholds.critical_missing_rate_max, 0.1);
    assert_eq!(policy.thresholds.blocked_rate_max, 0.2);
    assert_eq!(policy.thresholds.speculative_answer_rate_max, 0.0);
    assert!(policy.configured_via_env);
}

#[test]
fn determinism_rollout_policy_parser_normalizes_user_and_email_allowlists() {
    let policy = parse_determinism_rollout_policy_from_raw(
        Some("canary".to_string()),
        Some("5".to_string()),
        None,
        Some(" 8B5F8CB8-58AF-4C79-8472-BC68D7CF4366 ".to_string()),
        Some(" Alice@WithKura.com ; BOB@withkura.com ".to_string()),
        Some("false".to_string()),
        None,
        None,
        None,
        None,
        None,
        None,
    );
    assert!(
        policy
            .canary_user_allowlist
            .contains("8b5f8cb8-58af-4c79-8472-bc68d7cf4366")
    );
    assert!(policy.canary_email_allowlist.contains("alice@withkura.com"));
    assert!(policy.canary_email_allowlist.contains("bob@withkura.com"));
}

#[test]
fn determinism_rollout_canary_bucket_is_stable() {
    let sid = "session-canary-stability";
    let first = rollout_bucket_for_session(sid);
    let second = rollout_bucket_for_session(sid);
    assert_eq!(first, second);
    assert!(first < 100);
}

#[test]
fn rollout_guard_recommends_abort_on_metric_breach() {
    let policy = parse_determinism_rollout_policy_from_raw(
        Some("full".to_string()),
        Some("100".to_string()),
        None,
        None,
        None,
        Some("false".to_string()),
        Some("0.3".to_string()),
        Some("0.15".to_string()),
        Some("0.25".to_string()),
        Some("0.0".to_string()),
        Some("3".to_string()),
        Some("10".to_string()),
    );
    let mut state = RetrievalControlState::new(Instant::now());
    state.context_calls = 12;
    state.context_overflow_count = 8;
    state.context_critical_missing_count = 4;
    state.total_tool_calls = 20;
    state
        .abort_reasons
        .insert("startup_context_required".to_string(), 7);

    let guard = build_rollout_guard_payload_for_policy(&policy, "sid-rollout-abort", &state);
    assert_eq!(guard["status"], "abort_recommended");
    assert_eq!(guard["abort_required"], json!(false));
    let breached = guard["breached_thresholds"]
        .as_array()
        .expect("breached_thresholds must be array");
    assert!(
        breached
            .iter()
            .filter_map(|entry| entry.get("metric").and_then(Value::as_str))
            .any(|metric| metric == "overflow_rate")
    );
}

#[test]
fn rollout_abort_guard_blocks_only_when_enforced_and_active() {
    let policy = parse_determinism_rollout_policy_from_raw(
        Some("canary".to_string()),
        Some("0".to_string()),
        Some("sid-canary".to_string()),
        None,
        None,
        Some("true".to_string()),
        Some("0.2".to_string()),
        Some("0.2".to_string()),
        Some("0.2".to_string()),
        Some("0.0".to_string()),
        Some("3".to_string()),
        Some("10".to_string()),
    );
    let mut state = RetrievalControlState::new(Instant::now());
    state.context_calls = 10;
    state.context_overflow_count = 9;
    state.context_critical_missing_count = 5;
    state.total_tool_calls = 20;
    state
        .abort_reasons
        .insert("startup_context_required".to_string(), 8);

    let blocked = maybe_rollout_abort_guard_block_for_policy(
        &policy,
        "sid-canary",
        "kura_projection_get",
        &state,
    );
    assert!(
        blocked.is_some(),
        "active canary cohort must block on abort"
    );

    let non_canary = maybe_rollout_abort_guard_block_for_policy(
        &policy,
        "sid-non-canary",
        "kura_projection_get",
        &state,
    );
    assert!(
        non_canary.is_none(),
        "non-canary cohort must not be blocked when mode=canary"
    );
}

#[test]
fn rollout_guard_matches_user_or_email_allowlists_before_bucket() {
    let policy = parse_determinism_rollout_policy_from_raw(
        Some("canary".to_string()),
        Some("0".to_string()),
        None,
        Some("f64fc3a3-1ed6-40ec-bf64-cde32ea26ea9".to_string()),
        Some("antonia@example.com".to_string()),
        Some("false".to_string()),
        Some("0.9".to_string()),
        Some("0.9".to_string()),
        Some("0.9".to_string()),
        Some("0.9".to_string()),
        Some("1".to_string()),
        Some("1".to_string()),
    );

    let mut state = RetrievalControlState::new(Instant::now());
    state.total_tool_calls = 4;
    state.context_calls = 2;
    state.session_identity_user_id = Some("f64fc3a3-1ed6-40ec-bf64-cde32ea26ea9".to_string());
    let by_user = build_rollout_guard_payload_for_policy(&policy, "sid-outside", &state);
    assert_eq!(by_user["session_in_canary"], json!(true));
    assert_eq!(by_user["canary_match_source"], "user_allowlist");

    let mut state_email = RetrievalControlState::new(Instant::now());
    state_email.total_tool_calls = 4;
    state_email.context_calls = 2;
    state_email.session_identity_email = Some("antonia@example.com".to_string());
    let by_email = build_rollout_guard_payload_for_policy(&policy, "sid-outside", &state_email);
    assert_eq!(by_email["session_in_canary"], json!(true));
    assert_eq!(by_email["canary_match_source"], "email_allowlist");
}
