use chrono::Utc;
use clap::{Args, Subcommand, ValueEnum};
use serde_json::{Value, json};
use uuid::Uuid;

use crate::util::{
    api_request, exit_error, print_json_stderr, print_json_stdout, raw_api_request,
    read_json_from_file,
};

#[derive(Subcommand)]
pub enum AgentCommands {
    /// Get negotiated agent capabilities manifest
    Capabilities,
    /// Get the focused logging bootstrap contract or one intent-native logging recipe
    LoggingBootstrap {
        /// Optional intent recipe id (for example: log_conversation)
        #[arg(long)]
        intent: Option<String>,
    },
    /// Get agent context bundle (system + user profile + key dimensions)
    Context {
        /// Max exercise_progression projections to include (default: 5)
        #[arg(long)]
        exercise_limit: Option<u32>,
        /// Max strength_inference projections to include (default: 5)
        #[arg(long)]
        strength_limit: Option<u32>,
        /// Max custom projections to include (default: 10)
        #[arg(long)]
        custom_limit: Option<u32>,
        /// Optional task intent used for context ranking (e.g. "dunk progression")
        #[arg(long)]
        task_intent: Option<String>,
        /// Include deployment-static system config in response payload (default: API default=true)
        #[arg(long)]
        include_system: Option<bool>,
        /// Optional response token budget hint (min 400, max 12000)
        #[arg(long)]
        budget_tokens: Option<u32>,
    },
    /// Get deterministic section index for startup + targeted follow-up reads
    SectionIndex {
        /// Max exercise_progression projections to include (default: 5)
        #[arg(long)]
        exercise_limit: Option<u32>,
        /// Max strength_inference projections to include (default: 5)
        #[arg(long)]
        strength_limit: Option<u32>,
        /// Max custom projections to include (default: 10)
        #[arg(long)]
        custom_limit: Option<u32>,
        /// Optional task intent used for startup section derivation
        #[arg(long)]
        task_intent: Option<String>,
        /// Include deployment-static system config in response payload (default: API default=true)
        #[arg(long)]
        include_system: Option<bool>,
        /// Optional response token budget hint (min 400, max 12000)
        #[arg(long)]
        budget_tokens: Option<u32>,
    },
    /// Fetch exactly one context section (optionally paged and field-projected)
    SectionFetch {
        /// Stable section id from section-index
        #[arg(long)]
        section: String,
        /// Optional page size for paged sections (1..200)
        #[arg(long)]
        limit: Option<u32>,
        /// Optional opaque cursor for paged sections
        #[arg(long)]
        cursor: Option<String>,
        /// Optional comma-separated top-level fields to project
        #[arg(long)]
        fields: Option<String>,
        /// Optional task intent for startup section derivation
        #[arg(long)]
        task_intent: Option<String>,
    },
    /// Validate a draft answer against the authoritative date-bound coaching serving view
    AnswerAdmissibility {
        /// Current user request, preferably passed verbatim
        #[arg(long)]
        task_intent: String,
        /// Draft user-facing answer to validate
        #[arg(long)]
        draft_answer: String,
    },
    /// Evidence lineage operations
    Evidence {
        #[command(subcommand)]
        command: AgentEvidenceCommands,
    },
    /// Set user save-confirmation preference (persist-intent override)
    SetSaveConfirmationMode {
        /// auto | always | never
        #[arg(value_enum)]
        mode: SaveConfirmationMode,
    },
    /// Resolve visualization policy/output for a task intent
    ResolveVisualization(ResolveVisualizationArgs),
    /// Direct agent API access under /v1/agent/*
    #[command(hide = true)]
    Request(AgentRequestArgs),
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum SaveConfirmationMode {
    Auto,
    Always,
    Never,
}

#[cfg(test)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum SessionCompletionStatus {
    Ongoing,
    CompletedInBatch,
}

#[cfg(test)]
impl SessionCompletionStatus {
    fn as_str(self) -> &'static str {
        match self {
            SessionCompletionStatus::Ongoing => "ongoing",
            SessionCompletionStatus::CompletedInBatch => "completed_in_batch",
        }
    }
}

impl SaveConfirmationMode {
    fn as_str(self) -> &'static str {
        match self {
            SaveConfirmationMode::Auto => "auto",
            SaveConfirmationMode::Always => "always",
            SaveConfirmationMode::Never => "never",
        }
    }
}

#[derive(Subcommand)]
pub enum AgentEvidenceCommands {
    /// Explain lineage claims for one persisted event
    Event {
        /// Target event UUID
        #[arg(long)]
        event_id: Uuid,
    },
}

#[derive(Args)]
pub struct AgentRequestArgs {
    /// HTTP method (GET, POST, PUT, DELETE, PATCH)
    pub method: String,

    /// Agent path: relative (e.g. context) or absolute (/v1/agent/context)
    pub path: String,

    /// Request body as JSON string
    #[arg(long, short = 'd')]
    pub data: Option<String>,

    /// Read request body from file (use '-' for stdin)
    #[arg(long, short = 'f', conflicts_with = "data")]
    pub data_file: Option<String>,

    /// Query parameters (repeatable: key=value)
    #[arg(long, short = 'q')]
    pub query: Vec<String>,

    /// Extra headers (repeatable: Key:Value)
    #[arg(long, short = 'H')]
    pub header: Vec<String>,

    /// Skip pretty-printing (raw JSON for piping)
    #[arg(long)]
    pub raw: bool,

    /// Include HTTP status and headers in response wrapper
    #[arg(long, short = 'i')]
    pub include: bool,
}

#[derive(Args)]
pub struct ResolveVisualizationArgs {
    /// Full request payload JSON file for /v1/agent/visualization/resolve
    #[arg(long, conflicts_with = "task_intent")]
    pub request_file: Option<String>,

    /// Task intent (required unless --request-file is used)
    #[arg(long, required_unless_present = "request_file")]
    pub task_intent: Option<String>,

    /// auto | always | never
    #[arg(long)]
    pub user_preference_override: Option<String>,

    /// low | medium | high
    #[arg(long)]
    pub complexity_hint: Option<String>,

    /// Allow rich rendering formats when true (default: true)
    #[arg(long, default_value_t = true)]
    pub allow_rich_rendering: bool,

    /// Optional visualization_spec JSON file
    #[arg(long)]
    pub spec_file: Option<String>,

    /// Optional telemetry session id
    #[arg(long)]
    pub telemetry_session_id: Option<String>,
}

pub async fn run(api_url: &str, token: Option<&str>, command: AgentCommands) -> i32 {
    match command {
        AgentCommands::Capabilities => capabilities(api_url, token).await,
        AgentCommands::LoggingBootstrap { intent } => {
            logging_bootstrap(api_url, token, intent).await
        }
        AgentCommands::Context {
            exercise_limit,
            strength_limit,
            custom_limit,
            task_intent,
            include_system,
            budget_tokens,
        } => {
            context(
                api_url,
                token,
                exercise_limit,
                strength_limit,
                custom_limit,
                task_intent,
                include_system,
                budget_tokens,
            )
            .await
        }
        AgentCommands::SectionIndex {
            exercise_limit,
            strength_limit,
            custom_limit,
            task_intent,
            include_system,
            budget_tokens,
        } => {
            section_index(
                api_url,
                token,
                exercise_limit,
                strength_limit,
                custom_limit,
                task_intent,
                include_system,
                budget_tokens,
            )
            .await
        }
        AgentCommands::SectionFetch {
            section,
            limit,
            cursor,
            fields,
            task_intent,
        } => section_fetch(api_url, token, section, limit, cursor, fields, task_intent).await,
        AgentCommands::AnswerAdmissibility {
            task_intent,
            draft_answer,
        } => answer_admissibility(api_url, token, task_intent, draft_answer).await,
        AgentCommands::Evidence { command } => match command {
            AgentEvidenceCommands::Event { event_id } => {
                evidence_event(api_url, token, event_id).await
            }
        },
        AgentCommands::SetSaveConfirmationMode { mode } => {
            set_save_confirmation_mode(api_url, token, mode).await
        }
        AgentCommands::ResolveVisualization(args) => {
            resolve_visualization(api_url, token, args).await
        }
        AgentCommands::Request(args) => request(api_url, token, args).await,
    }
}

async fn capabilities(api_url: &str, token: Option<&str>) -> i32 {
    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/agent/capabilities",
        token,
        None,
        &[],
        &[],
        false,
        false,
    )
    .await
}

fn normalize_logging_bootstrap_intent(intent: &str) -> Option<String> {
    let normalized = intent.trim().to_ascii_lowercase();
    if normalized.is_empty() {
        None
    } else {
        Some(normalized)
    }
}

fn extract_logging_bootstrap_contract(capabilities: &Value) -> Option<Value> {
    capabilities
        .pointer("/task_bootstrap_contracts/logging")
        .cloned()
        .filter(|value| value.is_object())
}

fn available_logging_bootstrap_intents(contract: &Value) -> Vec<String> {
    let Some(recipes) = contract.get("intent_recipes").and_then(Value::as_array) else {
        return Vec::new();
    };
    recipes
        .iter()
        .filter_map(|recipe| recipe.get("intent_id").and_then(Value::as_str))
        .map(str::to_string)
        .collect()
}

fn build_logging_bootstrap_output(contract: &Value, intent: Option<&str>) -> Result<Value, Value> {
    let Some(intent) = intent else {
        return Ok(contract.clone());
    };
    let Some(normalized_intent) = normalize_logging_bootstrap_intent(intent) else {
        return Err(json!({
            "error": "usage_error",
            "message": "--intent must not be empty",
        }));
    };
    let recipes = contract
        .get("intent_recipes")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let Some(recipe) = recipes.into_iter().find(|recipe| {
        recipe
            .get("intent_id")
            .and_then(Value::as_str)
            .map(|value| value.eq_ignore_ascii_case(&normalized_intent))
            .unwrap_or(false)
    }) else {
        return Err(json!({
            "error": "usage_error",
            "message": format!("Unknown logging bootstrap intent: {intent}"),
            "available_intents": available_logging_bootstrap_intents(contract),
        }));
    };
    Ok(json!({
        "schema_version": contract.get("schema_version").cloned().unwrap_or(Value::Null),
        "task_family": contract.get("task_family").cloned().unwrap_or(Value::Null),
        "bootstrap_surface": contract.get("bootstrap_surface").cloned().unwrap_or(Value::Null),
        "intent_recipe": recipe,
        "save_states": contract.get("save_states").cloned().unwrap_or_else(|| json!([])),
        "upgrade_hints": contract.get("upgrade_hints").cloned().unwrap_or_else(|| json!([])),
        "integrity_guards": contract.get("integrity_guards").cloned().unwrap_or_else(|| json!([])),
    }))
}

#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct ResumeClarificationPrompt {
    prompt_id: Uuid,
    scope_kind: String,
    accepted_resolution_fields: Vec<String>,
}

async fn logging_bootstrap(api_url: &str, token: Option<&str>, intent: Option<String>) -> i32 {
    let (status, body) = raw_api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/agent/capabilities",
        token,
    )
    .await
    .unwrap_or_else(|error| {
        exit_error(
            &format!("Failed to fetch /v1/agent/capabilities for logging bootstrap: {error}"),
            Some("Retry once the API is reachable, or fall back to `kura agent capabilities`."),
        )
    });

    if !(200..=299).contains(&status) {
        print_json_stderr(&body);
        return if (400..500).contains(&status) { 1 } else { 2 };
    }

    let Some(contract) = extract_logging_bootstrap_contract(&body) else {
        exit_error(
            "agent capabilities response is missing task_bootstrap_contracts.logging",
            Some("Retry after `kura agent capabilities` succeeds, or inspect the full manifest."),
        );
    };

    match build_logging_bootstrap_output(&contract, intent.as_deref()) {
        Ok(output) => {
            print_json_stdout(&output);
            0
        }
        Err(error) => {
            print_json_stderr(&error);
            4
        }
    }
}

pub async fn context(
    api_url: &str,
    token: Option<&str>,
    exercise_limit: Option<u32>,
    strength_limit: Option<u32>,
    custom_limit: Option<u32>,
    task_intent: Option<String>,
    include_system: Option<bool>,
    budget_tokens: Option<u32>,
) -> i32 {
    let query = build_context_query(
        exercise_limit,
        strength_limit,
        custom_limit,
        task_intent,
        include_system,
        budget_tokens,
    );

    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/agent/context",
        token,
        None,
        &query,
        &[],
        false,
        false,
    )
    .await
}

pub async fn section_index(
    api_url: &str,
    token: Option<&str>,
    exercise_limit: Option<u32>,
    strength_limit: Option<u32>,
    custom_limit: Option<u32>,
    task_intent: Option<String>,
    include_system: Option<bool>,
    budget_tokens: Option<u32>,
) -> i32 {
    let query = build_context_query(
        exercise_limit,
        strength_limit,
        custom_limit,
        task_intent,
        include_system,
        budget_tokens,
    );
    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/agent/context/section-index",
        token,
        None,
        &query,
        &[],
        false,
        false,
    )
    .await
}

pub async fn section_fetch(
    api_url: &str,
    token: Option<&str>,
    section: String,
    limit: Option<u32>,
    cursor: Option<String>,
    fields: Option<String>,
    task_intent: Option<String>,
) -> i32 {
    let query = build_section_fetch_query(section, limit, cursor, fields, task_intent);
    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/agent/context/section-fetch",
        token,
        None,
        &query,
        &[],
        false,
        false,
    )
    .await
}

pub async fn answer_admissibility(
    api_url: &str,
    token: Option<&str>,
    task_intent: String,
    draft_answer: String,
) -> i32 {
    let body = json!({
        "task_intent": task_intent,
        "draft_answer": draft_answer,
    });
    api_request(
        api_url,
        reqwest::Method::POST,
        "/v1/agent/answer-admissibility",
        token,
        Some(body),
        &[],
        &[],
        false,
        false,
    )
    .await
}

async fn evidence_event(api_url: &str, token: Option<&str>, event_id: Uuid) -> i32 {
    let path = format!("/v1/agent/evidence/event/{event_id}");
    api_request(
        api_url,
        reqwest::Method::GET,
        &path,
        token,
        None,
        &[],
        &[],
        false,
        false,
    )
    .await
}

async fn set_save_confirmation_mode(
    api_url: &str,
    token: Option<&str>,
    mode: SaveConfirmationMode,
) -> i32 {
    let body = json!({
        "timestamp": Utc::now().to_rfc3339(),
        "event_type": "preference.set",
        "data": {
            "key": "save_confirmation_mode",
            "value": mode.as_str(),
        },
        "metadata": {
            "source": "cli",
            "agent": "kura-cli",
            "idempotency_key": Uuid::now_v7().to_string(),
        }
    });
    api_request(
        api_url,
        reqwest::Method::POST,
        "/v1/events",
        token,
        Some(body),
        &[],
        &[],
        false,
        false,
    )
    .await
}

async fn request(api_url: &str, token: Option<&str>, args: AgentRequestArgs) -> i32 {
    let method = parse_method(&args.method);
    let path = normalize_agent_path(&args.path);
    if is_blocked_agent_request_path(&path, &method) {
        exit_error(
            "Direct agent-request detours for ordinary workout logging are blocked in the CLI.",
            Some(
                "Use `kura record_activity` for activity logging instead of raw detours into old training routes.",
            ),
        );
    }
    let query = parse_query_pairs(&args.query);
    let headers = parse_headers(&args.header);
    let body = resolve_body(args.data.as_deref(), args.data_file.as_deref());

    api_request(
        api_url,
        method,
        &path,
        token,
        body,
        &query,
        &headers,
        args.raw,
        args.include,
    )
    .await
}

async fn resolve_visualization(
    api_url: &str,
    token: Option<&str>,
    args: ResolveVisualizationArgs,
) -> i32 {
    let body = if let Some(file) = args.request_file.as_deref() {
        match read_json_from_file(file) {
            Ok(v) => v,
            Err(e) => exit_error(
                &e,
                Some("Provide a valid JSON payload for /v1/agent/visualization/resolve."),
            ),
        }
    } else {
        let task_intent = match args.task_intent {
            Some(intent) if !intent.trim().is_empty() => intent,
            _ => exit_error(
                "task_intent is required unless --request-file is used.",
                Some("Use --task-intent or provide --request-file."),
            ),
        };

        let mut body = json!({
            "task_intent": task_intent,
            "allow_rich_rendering": args.allow_rich_rendering
        });
        if let Some(mode) = args.user_preference_override {
            body["user_preference_override"] = json!(mode);
        }
        if let Some(complexity) = args.complexity_hint {
            body["complexity_hint"] = json!(complexity);
        }
        if let Some(session_id) = args.telemetry_session_id {
            body["telemetry_session_id"] = json!(session_id);
        }
        if let Some(spec_file) = args.spec_file.as_deref() {
            let spec = match read_json_from_file(spec_file) {
                Ok(v) => v,
                Err(e) => exit_error(&e, Some("Provide a valid JSON visualization_spec payload.")),
            };
            body["visualization_spec"] = spec;
        }
        body
    };

    api_request(
        api_url,
        reqwest::Method::POST,
        "/v1/agent/visualization/resolve",
        token,
        Some(body),
        &[],
        &[],
        false,
        false,
    )
    .await
}

fn parse_method(raw: &str) -> reqwest::Method {
    match raw.to_uppercase().as_str() {
        "GET" => reqwest::Method::GET,
        "POST" => reqwest::Method::POST,
        "PUT" => reqwest::Method::PUT,
        "DELETE" => reqwest::Method::DELETE,
        "PATCH" => reqwest::Method::PATCH,
        "HEAD" => reqwest::Method::HEAD,
        "OPTIONS" => reqwest::Method::OPTIONS,
        other => exit_error(
            &format!("Unknown HTTP method: {other}"),
            Some("Supported methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"),
        ),
    }
}

fn normalize_agent_path(raw: &str) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        exit_error(
            "Agent path must not be empty.",
            Some("Use relative path like 'context' or absolute path '/v1/agent/context'."),
        );
    }

    if trimmed.starts_with("/v1/agent") {
        return trimmed.to_string();
    }
    if trimmed.starts_with("v1/agent") {
        return format!("/{trimmed}");
    }
    if trimmed.starts_with('/') {
        exit_error(
            &format!("Invalid agent path '{trimmed}'."),
            Some(
                "`kura agent request` only supports /v1/agent/* paths. Use public task commands instead of legacy structured detours.",
            ),
        );
    }

    format!("/v1/agent/{}", trimmed.trim_start_matches('/'))
}

fn is_blocked_agent_request_path(path: &str, method: &reqwest::Method) -> bool {
    if *method != reqwest::Method::POST {
        return false;
    }

    matches!(
        path.trim().to_ascii_lowercase().as_str(),
        "/v1/agent/exercise-resolve" | "/v4/agent/write-event" | "/v4/agent/write-correction"
    ) || path.trim().to_ascii_lowercase().starts_with("/v3/agent/")
}

fn parse_query_pairs(raw: &[String]) -> Vec<(String, String)> {
    raw.iter()
        .map(|entry| {
            entry.split_once('=').map_or_else(
                || {
                    exit_error(
                        &format!("Invalid query parameter: '{entry}'"),
                        Some("Format: key=value, e.g. --query event_type=meal.logged"),
                    )
                },
                |(k, v)| (k.to_string(), v.to_string()),
            )
        })
        .collect()
}

fn build_context_query(
    exercise_limit: Option<u32>,
    strength_limit: Option<u32>,
    custom_limit: Option<u32>,
    task_intent: Option<String>,
    include_system: Option<bool>,
    budget_tokens: Option<u32>,
) -> Vec<(String, String)> {
    let mut query = Vec::new();
    if let Some(v) = exercise_limit {
        query.push(("exercise_limit".to_string(), v.to_string()));
    }
    if let Some(v) = strength_limit {
        query.push(("strength_limit".to_string(), v.to_string()));
    }
    if let Some(v) = custom_limit {
        query.push(("custom_limit".to_string(), v.to_string()));
    }
    if let Some(v) = task_intent {
        query.push(("task_intent".to_string(), v));
    }
    if let Some(v) = include_system {
        query.push(("include_system".to_string(), v.to_string()));
    }
    if let Some(v) = budget_tokens {
        query.push(("budget_tokens".to_string(), v.to_string()));
    }
    query
}

fn build_section_fetch_query(
    section: String,
    limit: Option<u32>,
    cursor: Option<String>,
    fields: Option<String>,
    task_intent: Option<String>,
) -> Vec<(String, String)> {
    let section = section.trim();
    if section.is_empty() {
        exit_error(
            "section must not be empty",
            Some("Provide --section using an id from /v1/agent/context/section-index"),
        );
    }
    let mut query = vec![("section".to_string(), section.to_string())];
    if let Some(v) = limit {
        query.push(("limit".to_string(), v.to_string()));
    }
    if let Some(v) = cursor {
        query.push(("cursor".to_string(), v));
    }
    if let Some(v) = fields {
        query.push(("fields".to_string(), v));
    }
    if let Some(v) = task_intent {
        query.push(("task_intent".to_string(), v));
    }
    query
}

fn parse_headers(raw: &[String]) -> Vec<(String, String)> {
    raw.iter()
        .map(|entry| {
            entry.split_once(':').map_or_else(
                || {
                    exit_error(
                        &format!("Invalid header: '{entry}'"),
                        Some("Format: Key:Value, e.g. --header Content-Type:application/json"),
                    )
                },
                |(k, v)| (k.trim().to_string(), v.trim().to_string()),
            )
        })
        .collect()
}

fn resolve_body(data: Option<&str>, data_file: Option<&str>) -> Option<serde_json::Value> {
    if let Some(raw) = data {
        match serde_json::from_str(raw) {
            Ok(v) => return Some(v),
            Err(e) => exit_error(
                &format!("Invalid JSON in --data: {e}"),
                Some("Provide valid JSON string"),
            ),
        }
    }

    if let Some(file) = data_file {
        return match read_json_from_file(file) {
            Ok(v) => Some(v),
            Err(e) => exit_error(&e, Some("Provide a valid JSON file or use '-' for stdin")),
        };
    }

    None
}

#[cfg(test)]
fn unwrap_resume_payload<'a>(payload: &'a Value) -> &'a Value {
    if payload
        .get("schema_version")
        .and_then(Value::as_str)
        .is_some_and(|value| value == "write_preflight.v1")
    {
        return payload;
    }
    if let Some(body) = payload.get("body") {
        return unwrap_resume_payload(body);
    }
    if let Some(received) = payload.get("received") {
        return unwrap_resume_payload(received);
    }
    payload
}

#[cfg(test)]
fn extract_resume_clarification_prompts(payload: &Value) -> Vec<ResumeClarificationPrompt> {
    let root = unwrap_resume_payload(payload);
    root.get("blockers")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(|blocker| blocker.get("details"))
        .filter_map(|details| details.get("clarification_prompts"))
        .filter_map(Value::as_array)
        .flatten()
        .filter_map(|prompt| {
            let prompt_id = prompt.get("prompt_id")?.as_str()?;
            let prompt_id = Uuid::parse_str(prompt_id).ok()?;
            let scope_kind = prompt.get("scope_kind")?.as_str()?.trim().to_string();
            let accepted_resolution_fields = prompt
                .get("accepted_resolution_fields")
                .and_then(Value::as_array)
                .map(|fields| {
                    fields
                        .iter()
                        .filter_map(Value::as_str)
                        .map(str::trim)
                        .filter(|field| !field.is_empty())
                        .map(str::to_string)
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();
            Some(ResumeClarificationPrompt {
                prompt_id,
                scope_kind,
                accepted_resolution_fields,
            })
        })
        .collect()
}

#[cfg(test)]
fn select_resume_clarification_prompt(
    prompts: &[ResumeClarificationPrompt],
    explicit_prompt_id: Option<Uuid>,
) -> Result<ResumeClarificationPrompt, String> {
    if prompts.is_empty() {
        return Err(
            "resume_file does not contain a clarification_required blocker with clarification_prompts"
                .to_string(),
        );
    }

    if let Some(prompt_id) = explicit_prompt_id {
        return prompts
            .iter()
            .find(|prompt| prompt.prompt_id == prompt_id)
            .cloned()
            .ok_or_else(|| {
                format!("resume_file does not contain clarification prompt {prompt_id}")
            });
    }

    if prompts.len() == 1 {
        return Ok(prompts[0].clone());
    }

    Err(
        "resume_file contains multiple clarification prompts; provide --clarification-prompt-id"
            .to_string(),
    )
}

#[cfg(test)]
fn build_confirmation_payload(
    schema_version: &str,
    confirmation_token: &str,
    docs_hint: &str,
) -> serde_json::Value {
    let token = confirmation_token.trim();
    if token.is_empty() {
        exit_error(
            &format!("{schema_version} confirmation token must not be empty"),
            Some(docs_hint),
        );
    }
    json!({
        "schema_version": schema_version,
        "confirmed": true,
        "confirmed_at": Utc::now().to_rfc3339(),
        "confirmation_token": token,
    })
}

#[cfg(test)]
fn build_non_trivial_confirmation_from_token(confirmation_token: &str) -> serde_json::Value {
    build_confirmation_payload(
        "non_trivial_confirmation.v1",
        confirmation_token,
        "Use the confirmation token from claim_guard.non_trivial_confirmation_challenge.",
    )
}

#[cfg(test)]
fn build_high_impact_confirmation_from_token(confirmation_token: &str) -> serde_json::Value {
    build_confirmation_payload(
        "high_impact_confirmation.v1",
        confirmation_token,
        "Use the confirmation token from the prior high-impact confirm-first response.",
    )
}

#[cfg(test)]
const PLAN_UPDATE_VOLUME_DELTA_HIGH_IMPACT_ABS_GTE: f64 = 15.0;
#[cfg(test)]
const PLAN_UPDATE_INTENSITY_DELTA_HIGH_IMPACT_ABS_GTE: f64 = 10.0;
#[cfg(test)]
const PLAN_UPDATE_FREQUENCY_DELTA_HIGH_IMPACT_ABS_GTE: f64 = 2.0;
#[cfg(test)]
const PLAN_UPDATE_DURATION_DELTA_WEEKS_HIGH_IMPACT_ABS_GTE: f64 = 2.0;
#[cfg(test)]
const SCHEDULE_EXCEPTION_LOW_IMPACT_MAX_RANGE_DAYS: i64 = 14;

#[cfg(test)]
fn normalized_event_type(event: &Value) -> Option<String> {
    event
        .get("event_type")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(|value| value.to_lowercase())
}

#[cfg(test)]
fn is_always_high_impact_event_type(event_type: &str) -> bool {
    matches!(
        event_type.trim().to_lowercase().as_str(),
        "training_plan.created"
            | "training_plan.archived"
            | "projection_rule.created"
            | "projection_rule.archived"
            | "weight_target.set"
            | "sleep_target.set"
            | "nutrition_target.set"
            | "workflow.profile_completion.closed"
            | "workflow.profile_completion.override_granted"
            | "workflow.profile_completion.aborted"
            | "workflow.profile_completion.restarted"
    )
}

#[cfg(test)]
fn read_abs_f64(value: Option<&Value>) -> Option<f64> {
    let raw = value?;
    if let Some(number) = raw.as_f64() {
        return Some(number.abs());
    }
    if let Some(number) = raw.as_i64() {
        return Some((number as f64).abs());
    }
    if let Some(number) = raw.as_u64() {
        return Some((number as f64).abs());
    }
    raw.as_str()
        .and_then(|text| text.trim().parse::<f64>().ok())
        .map(f64::abs)
}

#[cfg(test)]
fn read_plan_delta_abs(data: &Value, keys: &[&str]) -> Option<f64> {
    for key in keys {
        if let Some(number) = read_abs_f64(data.get(*key)) {
            return Some(number);
        }
        if let Some(number) = read_abs_f64(data.get("delta").and_then(|delta| delta.get(*key))) {
            return Some(number);
        }
    }
    None
}

#[cfg(test)]
fn read_bool_like(value: Option<&Value>) -> Option<bool> {
    let raw = value?;
    if let Some(boolean) = raw.as_bool() {
        return Some(boolean);
    }
    if let Some(number) = raw.as_i64() {
        return match number {
            0 => Some(false),
            1 => Some(true),
            _ => None,
        };
    }
    raw.as_str()
        .and_then(|text| match text.trim().to_lowercase().as_str() {
            "true" | "yes" | "ja" | "1" | "on" | "active" => Some(true),
            "false" | "no" | "nein" | "0" | "off" | "inactive" => Some(false),
            _ => None,
        })
}

#[cfg(test)]
fn parse_local_date_value(value: Option<&Value>) -> Option<chrono::NaiveDate> {
    value
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .and_then(|value| chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").ok())
}

#[cfg(test)]
fn selector_has_explicit_occurrence_anchor(selector: &Value) -> bool {
    selector
        .get("occurrence_id")
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .is_some()
        || selector
            .get("occurrence_ids")
            .and_then(Value::as_array)
            .map(|values| {
                values.iter().any(|value| {
                    value
                        .as_str()
                        .map(str::trim)
                        .filter(|raw| !raw.is_empty())
                        .is_some()
                })
            })
            .unwrap_or(false)
}

#[cfg(test)]
fn selector_has_bounded_temporal_anchor(selector: &Value) -> bool {
    if selector_has_explicit_occurrence_anchor(selector) {
        return true;
    }
    if parse_local_date_value(selector.get("local_date").or_else(|| selector.get("date"))).is_some()
    {
        return true;
    }
    if selector
        .get("local_dates")
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .any(|value| parse_local_date_value(Some(value)).is_some())
        })
        .unwrap_or(false)
    {
        return true;
    }
    if parse_local_date_value(selector.get("week_of")).is_some() {
        return true;
    }

    let date_range = selector
        .get("date_range")
        .or_else(|| selector.get("between"))
        .unwrap_or(&Value::Null);
    let start = parse_local_date_value(date_range.get("start").or_else(|| date_range.get("from")));
    let end = parse_local_date_value(date_range.get("end").or_else(|| date_range.get("to")));
    match (start, end) {
        (Some(start), Some(end)) if end >= start => {
            (end - start).num_days() <= SCHEDULE_EXCEPTION_LOW_IMPACT_MAX_RANGE_DAYS
        }
        _ => false,
    }
}

#[cfg(test)]
fn schedule_exception_scope_is_high_impact(data: &Value) -> bool {
    let scope_value = data
        .get("change_scope")
        .or_else(|| data.get("update_scope"))
        .or_else(|| {
            data.get("scope")
                .and_then(|scope| scope.get("change_scope"))
        })
        .or_else(|| data.get("scope").and_then(|scope| scope.get("scope")))
        .and_then(Value::as_str)
        .map(|raw| raw.trim().to_lowercase());
    if matches!(
        scope_value.as_deref(),
        Some(
            "bulk"
                | "future_block"
                | "full_rewrite"
                | "template_rewrite"
                | "replace_future_schedule"
                | "mesocycle_reset"
                | "phase_shift"
        )
    ) {
        return true;
    }

    for key in ["days_affected", "occurrences_affected"] {
        if read_abs_f64(data.get("scope").and_then(|scope| scope.get(key))).unwrap_or(0.0)
            > SCHEDULE_EXCEPTION_LOW_IMPACT_MAX_RANGE_DAYS as f64
        {
            return true;
        }
    }
    if read_abs_f64(
        data.get("scope")
            .and_then(|scope| scope.get("weeks_affected")),
    )
    .unwrap_or(0.0)
        > 2.0
    {
        return true;
    }
    false
}

#[cfg(test)]
fn training_schedule_exception_is_high_impact(event_type: &str, data: &Value) -> bool {
    if read_bool_like(data.get("requires_explicit_confirmation")).unwrap_or(false)
        || read_bool_like(data.get("rewrite_template")).unwrap_or(false)
        || read_bool_like(data.get("replace_future_schedule")).unwrap_or(false)
        || read_bool_like(data.get("replace_entire_weekly_template")).unwrap_or(false)
        || read_bool_like(data.get("clear_all")).unwrap_or(false)
        || schedule_exception_scope_is_high_impact(data)
    {
        return true;
    }

    match event_type {
        "training_schedule.exception.cleared" => false,
        "training_schedule.exception.upsert" => {
            let selector = data.get("selector").unwrap_or(&Value::Null);
            !selector_has_bounded_temporal_anchor(selector)
        }
        _ => true,
    }
}

#[cfg(test)]
fn training_plan_update_is_high_impact(data: &Value) -> bool {
    let scope = data
        .get("change_scope")
        .or_else(|| data.get("update_scope"))
        .and_then(Value::as_str)
        .map(|raw| raw.trim().to_lowercase());
    if matches!(
        scope.as_deref(),
        Some(
            "full_rewrite" | "structural" | "major_adjustment" | "mesocycle_reset" | "phase_shift"
        )
    ) {
        return true;
    }

    if data
        .get("replace_entire_plan")
        .and_then(Value::as_bool)
        .unwrap_or(false)
        || data
            .get("archive_previous_plan")
            .and_then(Value::as_bool)
            .unwrap_or(false)
        || data
            .get("requires_explicit_confirmation")
            .and_then(Value::as_bool)
            .unwrap_or(false)
    {
        return true;
    }

    let volume_delta = read_plan_delta_abs(
        data,
        &[
            "volume_delta_pct",
            "planned_volume_delta_pct",
            "total_volume_delta_pct",
        ],
    )
    .unwrap_or(0.0);
    if volume_delta >= PLAN_UPDATE_VOLUME_DELTA_HIGH_IMPACT_ABS_GTE {
        return true;
    }

    let intensity_delta = read_plan_delta_abs(
        data,
        &[
            "intensity_delta_pct",
            "rir_delta",
            "rpe_delta",
            "effort_delta_pct",
        ],
    )
    .unwrap_or(0.0);
    if intensity_delta >= PLAN_UPDATE_INTENSITY_DELTA_HIGH_IMPACT_ABS_GTE {
        return true;
    }

    let frequency_delta = read_plan_delta_abs(
        data,
        &["frequency_delta_per_week", "sessions_per_week_delta"],
    )
    .unwrap_or(0.0);
    if frequency_delta >= PLAN_UPDATE_FREQUENCY_DELTA_HIGH_IMPACT_ABS_GTE {
        return true;
    }

    let duration_delta = read_plan_delta_abs(
        data,
        &["cycle_length_weeks_delta", "plan_duration_weeks_delta"],
    )
    .unwrap_or(0.0);
    duration_delta >= PLAN_UPDATE_DURATION_DELTA_WEEKS_HIGH_IMPACT_ABS_GTE
}

#[cfg(test)]
fn is_high_impact_event(event: &Value) -> bool {
    let Some(event_type) = normalized_event_type(event) else {
        return false;
    };
    if event_type == "training_plan.updated" {
        return event
            .get("data")
            .is_some_and(training_plan_update_is_high_impact);
    }
    if event_type == "training_schedule.exception.upsert"
        || event_type == "training_schedule.exception.cleared"
    {
        return event
            .get("data")
            .is_some_and(|data| training_schedule_exception_is_high_impact(&event_type, data));
    }
    is_always_high_impact_event_type(&event_type)
}

#[cfg(test)]
fn has_high_impact_events(events: &[Value]) -> bool {
    events.iter().any(is_high_impact_event)
}

#[cfg(test)]
fn extract_temporal_basis_from_context_body(body: &Value) -> Option<Value> {
    body.pointer("/meta/temporal_basis")
        .cloned()
        .filter(|value| value.is_object())
}

#[cfg(test)]
fn build_default_intent_handshake(
    events: &[serde_json::Value],
    intent_goal: Option<&str>,
    temporal_basis: serde_json::Value,
) -> serde_json::Value {
    let event_types: Vec<String> = events.iter().filter_map(normalized_event_type).collect();
    let planned_action = if event_types.is_empty() {
        "apply high-impact structured write update".to_string()
    } else {
        format!("write events: {}", event_types.join(", "))
    };

    json!({
        "schema_version": "intent_handshake.v1",
        "goal": intent_goal.unwrap_or("execute requested high-impact write safely"),
        "planned_action": planned_action,
        "assumptions": ["context and request intent are current"],
        "non_goals": ["no unrelated writes outside current task scope"],
        "impact_class": "high_impact_write",
        "success_criteria": "structured write returns verification and claim_guard for this action",
        "created_at": chrono::Utc::now().to_rfc3339(),
        "handshake_id": format!("cli-hs-{}", Uuid::now_v7()),
        "temporal_basis": temporal_basis,
    })
}

#[cfg(test)]
fn parse_targets(raw_targets: &[String]) -> Vec<serde_json::Value> {
    raw_targets
        .iter()
        .map(|raw| {
            let (projection_type, key) = raw.split_once(':').unwrap_or_else(|| {
                exit_error(
                    &format!("Invalid --target '{raw}'"),
                    Some("Use format projection_type:key, e.g. profile:overview"),
                )
            });
            let projection_type = projection_type.trim();
            let key = key.trim();
            if projection_type.is_empty() || key.is_empty() {
                exit_error(
                    &format!("Invalid --target '{raw}'"),
                    Some("projection_type and key must both be non-empty."),
                );
            }
            json!({
                "projection_type": projection_type,
                "key": key,
            })
        })
        .collect()
}

#[cfg(test)]
fn extract_events_array(events_payload: serde_json::Value) -> Vec<serde_json::Value> {
    if let Some(events) = events_payload.as_array() {
        return events.to_vec();
    }
    if let Some(events) = events_payload
        .get("events")
        .and_then(|value| value.as_array())
    {
        return events.to_vec();
    }
    exit_error(
        "events payload must be an array or object with events array",
        Some("Example: --events-file events.json where file is [{...}] or {\"events\": [{...}]}"),
    );
}

#[cfg(test)]
fn build_family_write_request(
    events: Vec<serde_json::Value>,
    parsed_targets: Vec<serde_json::Value>,
    verify_timeout_ms: Option<u64>,
    intent_handshake: Option<serde_json::Value>,
    high_impact_confirmation: Option<serde_json::Value>,
    non_trivial_confirmation: Option<serde_json::Value>,
    session_status: Option<SessionCompletionStatus>,
) -> serde_json::Value {
    let mut request = json!({
        "events": events,
        "read_after_write_targets": parsed_targets,
    });
    if let Some(timeout) = verify_timeout_ms {
        request["verify_timeout_ms"] = json!(timeout);
    }
    if let Some(intent_handshake) = intent_handshake {
        request["intent_handshake"] = intent_handshake;
    }
    if let Some(high_impact_confirmation) = high_impact_confirmation {
        request["high_impact_confirmation"] = high_impact_confirmation;
    }
    if let Some(non_trivial_confirmation) = non_trivial_confirmation {
        request["non_trivial_confirmation"] = non_trivial_confirmation;
    }
    if let Some(session_status) = session_status {
        request["session_completion"] = json!({
            "schema_version": "training_session_completion.v1",
            "status": session_status.as_str(),
        });
    }
    request
}

#[cfg(test)]
mod tests {
    use super::{
        ResumeClarificationPrompt, SaveConfirmationMode, SessionCompletionStatus,
        build_context_query, build_default_intent_handshake, build_family_write_request,
        build_high_impact_confirmation_from_token, build_logging_bootstrap_output,
        build_non_trivial_confirmation_from_token, build_section_fetch_query, extract_events_array,
        extract_logging_bootstrap_contract, extract_resume_clarification_prompts,
        extract_temporal_basis_from_context_body, has_high_impact_events,
        is_blocked_agent_request_path, normalize_agent_path, parse_method, parse_targets,
        select_resume_clarification_prompt,
    };
    use serde_json::json;
    use uuid::Uuid;

    #[test]
    fn normalize_agent_path_accepts_relative_path() {
        assert_eq!(
            normalize_agent_path("evidence/event/abc"),
            "/v1/agent/evidence/event/abc"
        );
    }

    #[test]
    fn normalize_agent_path_accepts_absolute_agent_path() {
        assert_eq!(
            normalize_agent_path("/v1/agent/context"),
            "/v1/agent/context"
        );
    }

    #[test]
    fn extract_logging_bootstrap_contract_reads_logging_node() {
        let capabilities = json!({
            "task_bootstrap_contracts": {
                "logging": {
                    "schema_version": "agent_logging_bootstrap_contract.v1",
                    "task_family": "logging"
                }
            }
        });
        let contract =
            extract_logging_bootstrap_contract(&capabilities).expect("logging bootstrap contract");
        assert_eq!(
            contract["schema_version"],
            json!("agent_logging_bootstrap_contract.v1")
        );
        assert_eq!(contract["task_family"], json!("logging"));
    }

    #[test]
    fn build_logging_bootstrap_output_selects_one_intent_recipe() {
        let contract = json!({
            "schema_version": "agent_logging_bootstrap_contract.v1",
            "task_family": "logging",
            "bootstrap_surface": "/v1/agent/capabilities",
            "intent_recipes": [
                {
                    "intent_id": "record_activity",
                    "endpoint": "/v4/agent/record-activity",
                    "cli_entrypoint": "kura record_activity"
                }
            ],
            "save_states": [{"save_state": "success"}],
            "upgrade_hints": [{"surface": "/v1/events"}],
            "integrity_guards": ["guard"]
        });
        let output = build_logging_bootstrap_output(&contract, Some("record_activity"))
            .expect("bootstrap output");
        assert_eq!(
            output["intent_recipe"]["intent_id"],
            json!("record_activity")
        );
        assert_eq!(
            output["intent_recipe"]["cli_entrypoint"],
            json!("kura record_activity")
        );
        assert_eq!(output["bootstrap_surface"], json!("/v1/agent/capabilities"));
        assert_eq!(output["save_states"][0]["save_state"], json!("success"));
    }

    #[test]
    fn extract_resume_clarification_prompts_reads_blocked_response_shape() {
        let prompt_id = Uuid::now_v7();
        let prompts = extract_resume_clarification_prompts(&json!({
            "schema_version": "write_preflight.v1",
            "status": "blocked",
            "blockers": [
                {
                    "code": "logging_intent_clarification_required",
                    "details": {
                        "clarification_prompts": [
                            {
                                "prompt_id": prompt_id,
                                "scope_kind": "training_vs_test",
                                "accepted_resolution_fields": ["resolved_route_family"]
                            }
                        ]
                    }
                }
            ]
        }));
        assert_eq!(
            prompts,
            vec![ResumeClarificationPrompt {
                prompt_id,
                scope_kind: "training_vs_test".to_string(),
                accepted_resolution_fields: vec!["resolved_route_family".to_string()],
            }]
        );
    }

    #[test]
    fn select_resume_clarification_prompt_accepts_single_prompt_without_explicit_id() {
        let prompt = ResumeClarificationPrompt {
            prompt_id: Uuid::now_v7(),
            scope_kind: "training_vs_test".to_string(),
            accepted_resolution_fields: vec!["resolved_route_family".to_string()],
        };
        let selected = select_resume_clarification_prompt(std::slice::from_ref(&prompt), None)
            .expect("prompt");
        assert_eq!(selected, prompt);
    }

    #[test]
    fn parse_method_accepts_standard_http_methods() {
        for method in &[
            "get", "GET", "post", "PUT", "delete", "patch", "head", "OPTIONS",
        ] {
            let parsed = parse_method(method);
            assert!(!parsed.as_str().is_empty());
        }
    }

    #[test]
    fn blocked_agent_request_paths_cover_legacy_workout_detours() {
        assert!(is_blocked_agent_request_path(
            "/v1/agent/exercise-resolve",
            &reqwest::Method::POST
        ));
        assert!(is_blocked_agent_request_path(
            "/v4/agent/write-event",
            &reqwest::Method::POST
        ));
        assert!(is_blocked_agent_request_path(
            "/v4/agent/write-correction",
            &reqwest::Method::POST
        ));
        assert!(!is_blocked_agent_request_path(
            "/v1/agent/context",
            &reqwest::Method::GET
        ));
    }

    #[test]
    fn parse_targets_accepts_projection_type_key_format() {
        let parsed = parse_targets(&[
            "profile:overview".to_string(),
            "training_timeline:overview".to_string(),
        ]);
        assert_eq!(parsed[0]["projection_type"], "profile");
        assert_eq!(parsed[0]["key"], "overview");
        assert_eq!(parsed[1]["projection_type"], "training_timeline");
        assert_eq!(parsed[1]["key"], "overview");
    }

    #[test]
    fn extract_events_array_supports_plain_array() {
        let events = extract_events_array(json!([
            {"event_type":"set.logged"},
            {"event_type":"metric.logged"}
        ]));
        assert_eq!(events.len(), 2);
    }

    #[test]
    fn extract_events_array_supports_object_wrapper() {
        let events = extract_events_array(json!({
            "events": [{"event_type":"set.logged"}]
        }));
        assert_eq!(events.len(), 1);
    }

    #[test]
    fn build_family_write_request_serializes_expected_fields() {
        let request = build_family_write_request(
            vec![json!({"event_type":"set.logged"})],
            vec![json!({"projection_type":"profile","key":"overview"})],
            Some(1200),
            None,
            None,
            None,
            None,
        );
        assert_eq!(request["events"].as_array().unwrap().len(), 1);
        assert_eq!(
            request["read_after_write_targets"]
                .as_array()
                .unwrap()
                .len(),
            1
        );
        assert_eq!(request["verify_timeout_ms"], 1200);
    }

    #[test]
    fn build_family_write_request_includes_non_trivial_confirmation_when_present() {
        let request = build_family_write_request(
            vec![json!({"event_type":"set.logged"})],
            vec![json!({"projection_type":"profile","key":"overview"})],
            None,
            None,
            None,
            Some(json!({
                "schema_version": "non_trivial_confirmation.v1",
                "confirmed": true,
                "confirmed_at": "2026-02-25T12:00:00Z",
                "confirmation_token": "abc"
            })),
            None,
        );
        assert_eq!(
            request["non_trivial_confirmation"]["schema_version"],
            "non_trivial_confirmation.v1"
        );
        assert_eq!(
            request["non_trivial_confirmation"]["confirmation_token"],
            "abc"
        );
    }

    #[test]
    fn build_family_write_request_includes_high_impact_fields_when_present() {
        let request = build_family_write_request(
            vec![json!({"event_type":"training_schedule.exception.upsert"})],
            vec![json!({"projection_type":"training_schedule","key":"effective"})],
            None,
            Some(json!({
                "schema_version": "intent_handshake.v1",
                "goal": "shift deload start",
                "impact_class": "high_impact_write",
                "temporal_basis": {
                    "schema_version": "temporal_basis.v1",
                    "context_generated_at": "2026-03-07T16:00:00Z",
                    "timezone": "Europe/Berlin",
                    "today_local_date": "2026-03-07"
                }
            })),
            Some(json!({
                "schema_version": "high_impact_confirmation.v1",
                "confirmed": true,
                "confirmed_at": "2026-03-07T16:05:00Z",
                "confirmation_token": "hi-123"
            })),
            None,
            None,
        );
        assert_eq!(
            request["intent_handshake"]["schema_version"],
            "intent_handshake.v1"
        );
        assert_eq!(
            request["high_impact_confirmation"]["confirmation_token"],
            "hi-123"
        );
    }

    #[test]
    fn build_family_write_request_includes_session_completion_when_present() {
        let request = build_family_write_request(
            vec![json!({"event_type":"set.logged"})],
            vec![json!({"projection_type":"training_timeline","key":"today"})],
            None,
            None,
            None,
            None,
            Some(SessionCompletionStatus::Ongoing),
        );
        assert_eq!(
            request["session_completion"]["schema_version"],
            "training_session_completion.v1"
        );
        assert_eq!(request["session_completion"]["status"], "ongoing");
    }

    #[test]
    fn build_non_trivial_confirmation_from_token_uses_expected_shape() {
        let payload = build_non_trivial_confirmation_from_token("tok-123");
        assert_eq!(payload["schema_version"], "non_trivial_confirmation.v1");
        assert_eq!(payload["confirmed"], true);
        assert_eq!(payload["confirmation_token"], "tok-123");
        assert!(payload["confirmed_at"].as_str().is_some());
    }

    #[test]
    fn build_high_impact_confirmation_from_token_uses_expected_shape() {
        let payload = build_high_impact_confirmation_from_token("tok-456");
        assert_eq!(payload["schema_version"], "high_impact_confirmation.v1");
        assert_eq!(payload["confirmed"], true);
        assert_eq!(payload["confirmation_token"], "tok-456");
        assert!(payload["confirmed_at"].as_str().is_some());
    }

    #[test]
    fn extract_temporal_basis_from_context_body_reads_meta_field() {
        let temporal_basis = extract_temporal_basis_from_context_body(&json!({
            "meta": {
                "temporal_basis": {
                    "schema_version": "temporal_basis.v1",
                    "timezone": "Europe/Berlin",
                    "today_local_date": "2026-03-07"
                }
            }
        }))
        .expect("temporal_basis must be extracted");
        assert_eq!(temporal_basis["schema_version"], "temporal_basis.v1");
        assert_eq!(temporal_basis["timezone"], "Europe/Berlin");
    }

    #[test]
    fn build_default_intent_handshake_uses_event_types_and_temporal_basis() {
        let handshake = build_default_intent_handshake(
            &[json!({"event_type":"training_schedule.exception.upsert"})],
            Some("shift today's session"),
            json!({
                "schema_version": "temporal_basis.v1",
                "context_generated_at": "2026-03-07T16:00:00Z",
                "timezone": "Europe/Berlin",
                "today_local_date": "2026-03-07"
            }),
        );
        assert_eq!(handshake["schema_version"], "intent_handshake.v1");
        assert_eq!(handshake["goal"], "shift today's session");
        assert_eq!(handshake["impact_class"], "high_impact_write");
        assert_eq!(
            handshake["temporal_basis"]["today_local_date"],
            "2026-03-07"
        );
    }

    #[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 save_confirmation_mode_serializes_expected_values() {
        assert_eq!(SaveConfirmationMode::Auto.as_str(), "auto");
        assert_eq!(SaveConfirmationMode::Always.as_str(), "always");
        assert_eq!(SaveConfirmationMode::Never.as_str(), "never");
    }

    #[test]
    fn build_context_query_includes_budget_tokens_when_present() {
        let query = build_context_query(
            Some(3),
            Some(2),
            Some(1),
            Some("readiness check".to_string()),
            Some(false),
            Some(900),
        );
        assert!(query.contains(&("exercise_limit".to_string(), "3".to_string())));
        assert!(query.contains(&("strength_limit".to_string(), "2".to_string())));
        assert!(query.contains(&("custom_limit".to_string(), "1".to_string())));
        assert!(query.contains(&("task_intent".to_string(), "readiness check".to_string())));
        assert!(query.contains(&("include_system".to_string(), "false".to_string())));
        assert!(query.contains(&("budget_tokens".to_string(), "900".to_string())));
    }

    #[test]
    fn build_context_query_supports_section_index_parity_params() {
        let query = build_context_query(
            Some(5),
            Some(5),
            Some(10),
            Some("startup".to_string()),
            Some(false),
            Some(1200),
        );
        assert!(query.contains(&("exercise_limit".to_string(), "5".to_string())));
        assert!(query.contains(&("strength_limit".to_string(), "5".to_string())));
        assert!(query.contains(&("custom_limit".to_string(), "10".to_string())));
        assert!(query.contains(&("task_intent".to_string(), "startup".to_string())));
        assert!(query.contains(&("include_system".to_string(), "false".to_string())));
        assert!(query.contains(&("budget_tokens".to_string(), "1200".to_string())));
    }

    #[test]
    fn build_section_fetch_query_serializes_optional_params() {
        let query = build_section_fetch_query(
            "projections.exercise_progression".to_string(),
            Some(50),
            Some("abc123".to_string()),
            Some("data,meta".to_string()),
            Some("bench plateau".to_string()),
        );
        assert_eq!(
            query,
            vec![
                (
                    "section".to_string(),
                    "projections.exercise_progression".to_string(),
                ),
                ("limit".to_string(), "50".to_string()),
                ("cursor".to_string(), "abc123".to_string()),
                ("fields".to_string(), "data,meta".to_string()),
                ("task_intent".to_string(), "bench plateau".to_string()),
            ]
        );
    }
}
