use serde_json::{json, Value};

use crate::app_config::{AppType, McpServer};
use crate::cli::i18n::texts;
use crate::cli::tui::form::strip_common_config_from_settings;
use crate::commands::workspace;
use crate::error::AppError;
use crate::openclaw_config::{
    set_agents_defaults, set_env_config, set_tools_config, OpenClawAgentsDefaults,
    OpenClawEnvConfig, OpenClawToolsConfig,
};
use crate::provider::Provider;
use crate::services::{McpService, PromptService, ProviderService};
use crate::settings::{set_webdav_sync_settings, WebDavSyncSettings};

use super::super::app::{CommonSnippetViewSource, EditorSubmit, ToastKind};
use super::super::data::{load_state, UiData};
use super::super::form::FormState;
use super::helpers::{
    refresh_openclaw_workspace_data, run_external_editor_for_current_editor, select_prompt_by_id,
};
use super::RuntimeActionContext;

fn is_codex_official_provider(provider: &Provider) -> bool {
    provider
        .meta
        .as_ref()
        .and_then(|meta| meta.codex_official)
        .unwrap_or(false)
        || provider.category.as_deref() == Some("official")
        || provider.website_url.as_deref() == Some("https://chatgpt.com/codex")
        || provider.name.trim().eq_ignore_ascii_case("OpenAI Official")
}

fn validate_provider_submit(
    app_type: &AppType,
    provider: &Provider,
    is_edit: bool,
) -> Option<&'static str> {
    if provider.name.trim().is_empty() {
        return Some(if is_edit {
            texts::tui_toast_provider_missing_name()
        } else {
            texts::tui_toast_provider_add_missing_fields()
        });
    }

    if matches!(app_type, AppType::Codex) && !is_codex_official_provider(provider) {
        let parsed = crate::cli::tui::form::parse_codex_config_snippet(
            provider
                .settings_config
                .get("config")
                .and_then(Value::as_str)
                .unwrap_or(""),
        );
        if parsed
            .base_url
            .as_deref()
            .is_none_or(|base_url| base_url.trim().is_empty())
        {
            return Some(texts::base_url_empty_error());
        }
    }

    None
}

pub(super) fn open_external(ctx: &mut RuntimeActionContext<'_>) -> Result<(), AppError> {
    ctx.terminal.with_terminal_restored(|| {
        run_external_editor_for_current_editor(ctx.app, crate::cli::editor::open_external_editor)
    })
}

enum CommonSnippetFormat {
    Empty,
    Formatted(String),
    InvalidJson(String),
    InvalidToml(String),
    NotObject,
    SerializeFailed(String),
}

fn canonical_common_snippet(app_type: &AppType, content: &str) -> CommonSnippetFormat {
    let edited = content.trim();
    if edited.is_empty() {
        return CommonSnippetFormat::Empty;
    }

    if matches!(app_type, AppType::Codex) {
        return match edited.parse::<toml_edit::DocumentMut>() {
            Ok(doc) => CommonSnippetFormat::Formatted(doc.to_string().trim().to_string()),
            Err(e) => CommonSnippetFormat::InvalidToml(e.to_string()),
        };
    }

    let value: Value = match serde_json::from_str(edited) {
        Ok(value) => value,
        Err(e) => return CommonSnippetFormat::InvalidJson(e.to_string()),
    };

    if !value.is_object() {
        return CommonSnippetFormat::NotObject;
    }

    match serde_json::to_string_pretty(&value) {
        Ok(pretty) => CommonSnippetFormat::Formatted(pretty),
        Err(e) => CommonSnippetFormat::SerializeFailed(e.to_string()),
    }
}

pub(super) fn format_common_snippet(
    ctx: &mut RuntimeActionContext<'_>,
    app_type: AppType,
) -> Result<(), AppError> {
    let Some(editor) = ctx.app.editor.as_mut() else {
        return Ok(());
    };
    let EditorSubmit::ConfigCommonSnippet {
        app_type: editor_app_type,
        ..
    } = &editor.submit
    else {
        return Ok(());
    };
    if editor_app_type != &app_type {
        return Ok(());
    }

    let formatted = match canonical_common_snippet(&app_type, &editor.text()) {
        CommonSnippetFormat::Empty => String::new(),
        CommonSnippetFormat::Formatted(value) => value,
        CommonSnippetFormat::InvalidToml(err) => {
            ctx.app.push_toast(
                texts::common_config_snippet_invalid_toml(&err),
                ToastKind::Error,
            );
            return Ok(());
        }
        CommonSnippetFormat::InvalidJson(err) => {
            ctx.app.push_toast(
                texts::common_config_snippet_invalid_json(&err),
                ToastKind::Error,
            );
            return Ok(());
        }
        CommonSnippetFormat::NotObject => {
            ctx.app
                .push_toast(texts::common_config_snippet_not_object(), ToastKind::Error);
            return Ok(());
        }
        CommonSnippetFormat::SerializeFailed(err) => {
            ctx.app
                .push_toast(texts::failed_to_serialize_json(&err), ToastKind::Error);
            return Ok(());
        }
    };

    if let Some(editor) = ctx.app.editor.as_mut() {
        editor.replace_text(formatted);
    }
    ctx.app
        .push_toast(texts::common_config_snippet_formatted(), ToastKind::Success);
    Ok(())
}

pub(super) fn extract_common_snippet_into_editor(
    ctx: &mut RuntimeActionContext<'_>,
    app_type: AppType,
) -> Result<(), AppError> {
    let source = ctx
        .app
        .editor
        .as_ref()
        .and_then(|editor| match &editor.submit {
            EditorSubmit::ConfigCommonSnippet {
                app_type: editor_app_type,
                source,
            } if editor_app_type == &app_type => Some(*source),
            _ => None,
        });
    if !matches!(
        source,
        Some(crate::cli::tui::app::CommonSnippetViewSource::ProviderForm)
    ) {
        return Ok(());
    }

    let settings_config = {
        let Some(FormState::ProviderAdd(provider)) = ctx.app.form.as_ref() else {
            return Ok(());
        };

        if provider.app_type != app_type {
            return Ok(());
        }

        let provider_value = match provider
            .to_provider_json_value_with_common_config(&ctx.data.config.common_snippet)
        {
            Ok(value) => value,
            Err(err) => {
                ctx.app.push_toast(err, ToastKind::Error);
                return Ok(());
            }
        };
        provider_value
            .get("settingsConfig")
            .cloned()
            .unwrap_or_else(|| json!({}))
    };

    let extracted = ProviderService::extract_common_config_snippet_from_settings(
        app_type.clone(),
        &settings_config,
    )?;
    if !crate::cli::tui::form::ProviderAddFormState::snippet_has_effective_common_config(
        &app_type, &extracted,
    ) {
        ctx.app.push_toast(
            texts::common_config_snippet_extract_empty(),
            ToastKind::Info,
        );
        return Ok(());
    }

    if let Some(editor) = ctx.app.editor.as_mut() {
        editor.replace_text(extracted);
    }
    ctx.app
        .push_toast(texts::common_config_snippet_extracted(), ToastKind::Success);
    Ok(())
}

pub(super) fn submit(
    ctx: &mut RuntimeActionContext<'_>,
    submit: EditorSubmit,
    content: String,
) -> Result<(), AppError> {
    match submit {
        EditorSubmit::PromptCreate {
            id,
            name,
            description,
        } => submit_prompt_create(ctx, id, name, description, content),
        EditorSubmit::PromptEdit { id } => submit_prompt_edit(ctx, id, content),
        EditorSubmit::ProviderFormApplyJson => submit_provider_form_apply_json(ctx, content),
        EditorSubmit::ProviderFormApplyOpenClawModels => {
            submit_provider_form_apply_openclaw_models(ctx, content)
        }
        EditorSubmit::ProviderFormApplyUsageScriptCode => {
            submit_provider_form_apply_usage_script_code(ctx, content)
        }
        EditorSubmit::ProviderFormApplyCodexAuth => {
            submit_provider_form_apply_codex_auth(ctx, content)
        }
        EditorSubmit::ProviderFormApplyCodexConfigToml => {
            submit_provider_form_apply_codex_config_toml(ctx, content)
        }
        EditorSubmit::ProviderAdd => submit_provider_add(ctx, content),
        EditorSubmit::ProviderEdit { id } => submit_provider_edit(ctx, id, content),
        EditorSubmit::McpAdd => submit_mcp_add(ctx, content),
        EditorSubmit::McpEdit { id } => submit_mcp_edit(ctx, id, content),
        EditorSubmit::ConfigCommonSnippet { app_type, source } => {
            submit_config_common_snippet(ctx, app_type, source, content)
        }
        EditorSubmit::OpenClawWorkspaceFile { filename } => {
            submit_openclaw_workspace_file(ctx, filename, content)
        }
        EditorSubmit::OpenClawDailyMemoryFile { filename } => {
            submit_openclaw_daily_memory_file(ctx, filename, content)
        }
        EditorSubmit::HermesMemory { kind } => submit_hermes_memory(ctx, kind, content),
        EditorSubmit::ConfigOpenClawEnv => submit_openclaw_env(ctx, content),
        EditorSubmit::ConfigOpenClawTools => submit_openclaw_tools(ctx, content),
        EditorSubmit::ConfigOpenClawAgents => submit_openclaw_agents(ctx, content),
        EditorSubmit::ConfigWebDavSettings => submit_webdav_settings(ctx, content),
    }
}

fn submit_hermes_memory(
    ctx: &mut RuntimeActionContext<'_>,
    kind: crate::hermes_config::MemoryKind,
    content: String,
) -> Result<(), AppError> {
    crate::hermes_config::write_memory(kind, &content)?;
    ctx.app.editor = None;
    ctx.app.push_toast(
        texts::tui_hermes_memory_saved(super::config::hermes_memory_kind_label(kind)),
        ToastKind::Success,
    );
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

fn submit_prompt_create(
    ctx: &mut RuntimeActionContext<'_>,
    id: String,
    name: String,
    description: Option<String>,
    content: String,
) -> Result<(), AppError> {
    let state = load_state()?;
    let prompt = match PromptService::create_prompt_with_id(
        &state,
        ctx.app.app_type.clone(),
        Some(&id),
        &name,
        description.as_deref(),
        &content,
    ) {
        Ok(prompt) => prompt,
        Err(err) => {
            ctx.app.push_toast(err.to_string(), ToastKind::Error);
            return Ok(());
        }
    };

    ctx.app.editor = None;
    ctx.app.form = None;
    ctx.app
        .push_toast(texts::tui_toast_prompt_created(), ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    select_prompt_by_id(ctx.app, ctx.data, &prompt.id);
    Ok(())
}

fn submit_openclaw_workspace_file(
    ctx: &mut RuntimeActionContext<'_>,
    filename: String,
    content: String,
) -> Result<(), AppError> {
    workspace::write_workspace_file(filename.clone(), content).map_err(|err| {
        AppError::Message(texts::tui_openclaw_workspace_save_failed(&filename, &err))
    })?;
    ctx.app.editor = None;
    ctx.app.push_toast(
        texts::tui_openclaw_workspace_saved(&filename),
        ToastKind::Success,
    );
    refresh_openclaw_workspace_data(ctx.app, ctx.data).map_err(|err| {
        AppError::Message(texts::tui_openclaw_workspace_refresh_failed(
            &err.to_string(),
        ))
    })
}

fn submit_openclaw_daily_memory_file(
    ctx: &mut RuntimeActionContext<'_>,
    filename: String,
    content: String,
) -> Result<(), AppError> {
    workspace::write_daily_memory_file(filename.clone(), content).map_err(|err| {
        AppError::Message(texts::tui_openclaw_daily_memory_save_failed(
            &filename, &err,
        ))
    })?;
    ctx.app.editor = None;
    ctx.app.push_toast(
        texts::tui_openclaw_daily_memory_saved(&filename),
        ToastKind::Success,
    );
    refresh_openclaw_workspace_data(ctx.app, ctx.data).map_err(|err| {
        AppError::Message(texts::tui_openclaw_daily_memory_refresh_failed(
            &err.to_string(),
        ))
    })
}

fn submit_openclaw_env(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let env: OpenClawEnvConfig = match serde_json::from_str(&content) {
        Ok(env) => env,
        Err(err) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&err.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    set_env_config(&env)?;
    ctx.app.editor = None;
    ctx.app.push_toast(
        texts::tui_toast_openclaw_config_saved(texts::tui_config_item_openclaw_env()),
        ToastKind::Success,
    );
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

fn submit_openclaw_tools(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let tools: OpenClawToolsConfig = match serde_json::from_str(&content) {
        Ok(tools) => tools,
        Err(err) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&err.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if let Err(err) = set_tools_config(&tools) {
        ctx.app.push_toast(
            texts::tui_toast_openclaw_tools_save_failed_detail(&err.to_string()),
            ToastKind::Error,
        );
        log::warn!("failed to save OpenClaw tools config: {err}");
        return Ok(());
    }

    let previous_form = ctx.app.openclaw_tools_form.clone();
    ctx.app.editor = None;
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    let mut form = crate::cli::tui::app::OpenClawToolsFormState::from_snapshot(
        ctx.data.config.openclaw_tools.as_ref(),
    );
    if let Some(previous_form) = previous_form.as_ref() {
        form.restore_position(previous_form);
    }
    ctx.app.openclaw_tools_form = Some(form);
    if ctx
        .app
        .toast
        .as_ref()
        .is_some_and(|toast| toast.kind == ToastKind::Error)
    {
        ctx.app.toast = None;
    }
    Ok(())
}

fn submit_openclaw_agents(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let defaults: OpenClawAgentsDefaults = match serde_json::from_str(&content) {
        Ok(defaults) => defaults,
        Err(err) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&err.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if let Err(err) = set_agents_defaults(&defaults) {
        ctx.app.push_toast(
            texts::tui_toast_openclaw_agents_save_failed_detail(&err.to_string()),
            ToastKind::Error,
        );
        log::warn!("failed to save OpenClaw agents config: {err}");
        return Ok(());
    }

    let previous_form = ctx.app.openclaw_agents_form.clone();
    ctx.app.editor = None;
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    let mut form = crate::cli::tui::app::OpenClawAgentsFormState::from_snapshot(
        ctx.data.config.openclaw_agents_defaults.as_ref(),
    );
    if let Some(previous_form) = previous_form.as_ref() {
        form.restore_position(previous_form);
    }
    ctx.app.openclaw_agents_form = Some(form);
    if ctx
        .app
        .toast
        .as_ref()
        .is_some_and(|toast| toast.kind == ToastKind::Error)
    {
        ctx.app.toast = None;
    }
    Ok(())
}

fn submit_prompt_edit(
    ctx: &mut RuntimeActionContext<'_>,
    id: String,
    content: String,
) -> Result<(), AppError> {
    let state = load_state()?;
    let prompts = PromptService::get_prompts(&state, ctx.app.app_type.clone())?;
    let Some(mut prompt) = prompts.get(&id).cloned() else {
        ctx.app
            .push_toast(texts::tui_toast_prompt_not_found(&id), ToastKind::Error);
        return Ok(());
    };

    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs() as i64;
    prompt.content = content;
    prompt.updated_at = Some(timestamp);

    if let Err(err) = PromptService::upsert_prompt(&state, ctx.app.app_type.clone(), &id, prompt) {
        ctx.app.push_toast(err.to_string(), ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    ctx.app
        .push_toast(texts::tui_toast_prompt_edit_finished(), ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

fn submit_provider_form_apply_json(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let mut settings_value: Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if !settings_value.is_object() {
        ctx.app
            .push_toast(texts::tui_toast_json_must_be_object(), ToastKind::Error);
        return Ok(());
    }

    let provider_value = match ctx.app.form.as_ref() {
        Some(FormState::ProviderAdd(form)) => {
            if form.should_strip_common_config_from_applied_settings_json() {
                if let Err(err) = strip_common_config_from_settings(
                    &form.app_type,
                    &mut settings_value,
                    &ctx.data.config.common_snippet,
                ) {
                    ctx.app.push_toast(err, ToastKind::Error);
                    return Ok(());
                }
            }

            let mut provider_value = form.to_provider_json_value();
            if let Some(obj) = provider_value.as_object_mut() {
                obj.insert("settingsConfig".to_string(), settings_value.clone());
            }
            Some(provider_value)
        }
        _ => None,
    };

    if let Some(provider_value) = provider_value {
        let apply_result = match ctx.app.form.as_mut() {
            Some(FormState::ProviderAdd(form)) => {
                form.apply_provider_json_value_to_fields(provider_value)
            }
            _ => Ok(()),
        };

        if let Err(err) = apply_result {
            ctx.app.push_toast(err, ToastKind::Error);
            return Ok(());
        }
    }
    ctx.app.editor = None;
    Ok(())
}

fn submit_provider_form_apply_openclaw_models(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let models_value: Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if !models_value.is_array() {
        ctx.app
            .push_toast(texts::tui_toast_json_must_be_array(), ToastKind::Error);
        return Ok(());
    }

    let apply_result = match ctx.app.form.as_mut() {
        Some(FormState::ProviderAdd(form)) => form.apply_openclaw_models_value(models_value),
        _ => Ok(()),
    };

    if let Err(err) = apply_result {
        ctx.app.push_toast(err, ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    Ok(())
}

fn submit_provider_form_apply_usage_script_code(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    if let Some(FormState::ProviderAdd(form)) = ctx.app.form.as_mut() {
        form.usage_query_code = content;
        form.touch_usage_query();
    }
    ctx.app.editor = None;
    Ok(())
}

fn submit_provider_form_apply_codex_auth(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let auth_value: Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if !auth_value.is_object() {
        ctx.app
            .push_toast(texts::tui_toast_json_must_be_object(), ToastKind::Error);
        return Ok(());
    }

    let provider_value = match ctx.app.form.as_ref() {
        Some(FormState::ProviderAdd(form)) => {
            let mut provider_value = form.to_provider_json_value();
            if let Some(settings_value) = provider_value
                .as_object_mut()
                .and_then(|obj| obj.get_mut("settingsConfig"))
            {
                if !settings_value.is_object() {
                    *settings_value = json!({});
                }
                if let Some(settings_obj) = settings_value.as_object_mut() {
                    settings_obj.insert("auth".to_string(), auth_value);
                }
            }
            Some(provider_value)
        }
        _ => None,
    };

    if let Some(provider_value) = provider_value {
        let apply_result = match ctx.app.form.as_mut() {
            Some(FormState::ProviderAdd(form)) => {
                form.apply_provider_json_value_to_fields(provider_value)
            }
            _ => Ok(()),
        };

        if let Err(err) = apply_result {
            ctx.app.push_toast(err, ToastKind::Error);
            return Ok(());
        }
    }

    ctx.app.editor = None;
    Ok(())
}

fn submit_provider_form_apply_codex_config_toml(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    use toml_edit::DocumentMut;

    let config_text = if content.trim().is_empty() {
        String::new()
    } else {
        let doc: DocumentMut = match content.parse() {
            Ok(doc) => doc,
            Err(e) => {
                ctx.app.push_toast(
                    texts::common_config_snippet_invalid_toml(&e.to_string()),
                    ToastKind::Error,
                );
                return Ok(());
            }
        };
        doc.to_string()
    };

    let provider_value = match ctx.app.form.as_ref() {
        Some(FormState::ProviderAdd(form)) => {
            let mut provider_value = form.to_provider_json_value();
            if let Some(settings_value) = provider_value
                .as_object_mut()
                .and_then(|obj| obj.get_mut("settingsConfig"))
            {
                if !settings_value.is_object() {
                    *settings_value = json!({});
                }
                if let Some(settings_obj) = settings_value.as_object_mut() {
                    settings_obj.insert("config".to_string(), Value::String(config_text));
                }
            }
            Some(provider_value)
        }
        _ => None,
    };

    if let Some(provider_value) = provider_value {
        let apply_result = match ctx.app.form.as_mut() {
            Some(FormState::ProviderAdd(form)) => {
                form.apply_provider_json_value_to_fields(provider_value)
            }
            _ => Ok(()),
        };

        if let Err(err) = apply_result {
            ctx.app.push_toast(err, ToastKind::Error);
            return Ok(());
        }
    }

    ctx.app.editor = None;
    Ok(())
}

fn submit_provider_add(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let mut provider: Provider = match serde_json::from_str(&content) {
        Ok(p) => p,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if let Some(message) = validate_provider_submit(&ctx.app.app_type, &provider, false) {
        ctx.app.push_toast(message, ToastKind::Warning);
        return Ok(());
    }

    let state = load_state()?;
    let existing_ids = {
        let config = state.config.read().map_err(AppError::from)?;
        config
            .get_manager(&ctx.app.app_type)
            .map(|manager| manager.providers.keys().cloned().collect::<Vec<_>>())
            .unwrap_or_default()
    };
    let Some(provider_id) = crate::cli::tui::form::resolve_provider_id_for_submit(
        &provider.name,
        &provider.id,
        &existing_ids,
    ) else {
        ctx.app.push_toast(
            texts::tui_toast_provider_add_missing_fields(),
            ToastKind::Warning,
        );
        return Ok(());
    };
    provider.id = provider_id;

    match ProviderService::add(&state, ctx.app.app_type.clone(), provider) {
        Ok(true) => {
            ctx.app.editor = None;
            ctx.app.form = None;
            ctx.app
                .push_toast(texts::tui_toast_provider_add_finished(), ToastKind::Success);
            *ctx.data = UiData::load(&ctx.app.app_type)?;
        }
        Ok(false) => {
            ctx.app
                .push_toast(texts::tui_toast_provider_add_failed(), ToastKind::Error);
        }
        Err(err) => {
            ctx.app.push_toast(err.to_string(), ToastKind::Error);
        }
    }

    Ok(())
}

fn submit_provider_edit(
    ctx: &mut RuntimeActionContext<'_>,
    id: String,
    content: String,
) -> Result<(), AppError> {
    let mut provider: Provider = match serde_json::from_str(&content) {
        Ok(p) => p,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };
    provider.id = id.clone();

    if let Some(message) = validate_provider_submit(&ctx.app.app_type, &provider, true) {
        ctx.app.push_toast(message, ToastKind::Warning);
        return Ok(());
    }

    let state = load_state()?;
    let result = ProviderService::update(&state, ctx.app.app_type.clone(), provider);

    if let Err(err) = result {
        ctx.app.push_toast(err.to_string(), ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    ctx.app.form = None;
    ctx.app.push_toast(
        texts::tui_toast_provider_edit_finished(),
        ToastKind::Success,
    );
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}
fn submit_mcp_add(ctx: &mut RuntimeActionContext<'_>, content: String) -> Result<(), AppError> {
    let server: McpServer = match serde_json::from_str(&content) {
        Ok(s) => s,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };

    if server.id.trim().is_empty() || server.name.trim().is_empty() {
        ctx.app
            .push_toast(texts::tui_toast_mcp_missing_fields(), ToastKind::Warning);
        return Ok(());
    }

    let state = load_state()?;
    if let Err(err) = McpService::upsert_server(&state, server) {
        ctx.app.push_toast(err.to_string(), ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    ctx.app.form = None;
    ctx.app
        .push_toast(texts::tui_toast_mcp_upserted(), ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

fn submit_mcp_edit(
    ctx: &mut RuntimeActionContext<'_>,
    id: String,
    content: String,
) -> Result<(), AppError> {
    let mut server: McpServer = match serde_json::from_str(&content) {
        Ok(s) => s,
        Err(e) => {
            ctx.app.push_toast(
                texts::tui_toast_invalid_json(&e.to_string()),
                ToastKind::Error,
            );
            return Ok(());
        }
    };
    server.id = id.clone();

    if server.name.trim().is_empty() {
        ctx.app
            .push_toast(texts::tui_toast_mcp_missing_fields(), ToastKind::Warning);
        return Ok(());
    }

    let state = load_state()?;
    if let Err(err) = McpService::upsert_server(&state, server) {
        ctx.app.push_toast(err.to_string(), ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    ctx.app.form = None;
    ctx.app
        .push_toast(texts::tui_toast_mcp_upserted(), ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

fn submit_config_common_snippet(
    ctx: &mut RuntimeActionContext<'_>,
    app_type: AppType,
    source: CommonSnippetViewSource,
    content: String,
) -> Result<(), AppError> {
    let (next_snippet, toast) = match canonical_common_snippet(&app_type, &content) {
        CommonSnippetFormat::Empty => (None, texts::common_config_snippet_cleared()),
        CommonSnippetFormat::Formatted(value) => {
            (Some(value), texts::common_config_snippet_saved())
        }
        CommonSnippetFormat::InvalidToml(err) => {
            ctx.app.push_toast(
                texts::common_config_snippet_invalid_toml(&err),
                ToastKind::Error,
            );
            return Ok(());
        }
        CommonSnippetFormat::InvalidJson(err) => {
            ctx.app.push_toast(
                texts::common_config_snippet_invalid_json(&err),
                ToastKind::Error,
            );
            return Ok(());
        }
        CommonSnippetFormat::NotObject => {
            ctx.app
                .push_toast(texts::common_config_snippet_not_object(), ToastKind::Error);
            return Ok(());
        }
        CommonSnippetFormat::SerializeFailed(err) => {
            ctx.app
                .push_toast(texts::failed_to_serialize_json(&err), ToastKind::Error);
            return Ok(());
        }
    };

    let state = load_state()?;
    let service_result = if let Some(snippet) = next_snippet.clone() {
        ProviderService::set_common_config_snippet(&state, app_type.clone(), Some(snippet))
    } else {
        ProviderService::clear_common_config_snippet(&state, app_type.clone())
    };
    if let Err(err) = service_result {
        ctx.app.push_toast(err.to_string(), ToastKind::Error);
        return Ok(());
    }

    ctx.app.editor = None;
    ctx.app.push_toast(toast, ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    if matches!(source, CommonSnippetViewSource::Global) {
        ctx.app.overlay = crate::cli::tui::app::Overlay::None;
    }
    Ok(())
}

fn submit_webdav_settings(
    ctx: &mut RuntimeActionContext<'_>,
    content: String,
) -> Result<(), AppError> {
    let edited = content.trim();
    if edited.is_empty() {
        set_webdav_sync_settings(None)?;
        ctx.app.editor = None;
        ctx.app.push_toast(
            texts::tui_toast_webdav_settings_cleared(),
            ToastKind::Success,
        );
        *ctx.data = UiData::load(&ctx.app.app_type)?;
        return Ok(());
    }

    let cfg: WebDavSyncSettings = serde_json::from_str(edited)
        .map_err(|e| AppError::Message(texts::tui_toast_invalid_json(&e.to_string())))?;
    set_webdav_sync_settings(Some(cfg))?;

    ctx.app.editor = None;
    ctx.app
        .push_toast(texts::tui_toast_webdav_settings_saved(), ToastKind::Success);
    *ctx.data = UiData::load(&ctx.app.app_type)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
    use serial_test::serial;
    use std::collections::HashMap;
    use std::ffi::OsString;
    use std::path::Path;
    use tempfile::{tempdir, TempDir};

    use crate::app_config::AppType;
    use crate::cli::tui::app::{Action, App, Focus, Toast};
    use crate::cli::tui::route::Route;
    use crate::cli::tui::runtime_systems::RequestTracker;
    use crate::cli::tui::terminal::TuiTerminal;
    use crate::openclaw_config::{write_openclaw_config_source, OpenClawModelCatalogEntry};
    use crate::settings::{get_settings, update_settings, AppSettings};
    use crate::test_support::{
        lock_test_home_and_settings, set_test_home_override, TestHomeSettingsLock,
    };

    fn key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    fn ctrl(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::CONTROL)
    }

    struct EnvGuard {
        _lock: TestHomeSettingsLock,
        old_home: Option<OsString>,
        old_userprofile: Option<OsString>,
        old_config_dir: Option<OsString>,
    }

    impl EnvGuard {
        fn set_home(home: &Path) -> Self {
            let lock = lock_test_home_and_settings();
            let old_home = std::env::var_os("HOME");
            let old_userprofile = std::env::var_os("USERPROFILE");
            let old_config_dir = std::env::var_os("CC_SWITCH_CONFIG_DIR");
            std::env::set_var("HOME", home);
            std::env::set_var("USERPROFILE", home);
            std::env::set_var("CC_SWITCH_CONFIG_DIR", home.join(".cc-switch"));
            set_test_home_override(Some(home));
            crate::settings::reload_test_settings();
            Self {
                _lock: lock,
                old_home,
                old_userprofile,
                old_config_dir,
            }
        }
    }

    impl Drop for EnvGuard {
        fn drop(&mut self) {
            match &self.old_home {
                Some(value) => std::env::set_var("HOME", value),
                None => std::env::remove_var("HOME"),
            }
            match &self.old_userprofile {
                Some(value) => std::env::set_var("USERPROFILE", value),
                None => std::env::remove_var("USERPROFILE"),
            }
            match &self.old_config_dir {
                Some(value) => std::env::set_var("CC_SWITCH_CONFIG_DIR", value),
                None => std::env::remove_var("CC_SWITCH_CONFIG_DIR"),
            }
            set_test_home_override(self.old_home.as_deref().map(Path::new));
            crate::settings::reload_test_settings();
        }
    }

    struct SettingsGuard {
        previous: AppSettings,
    }

    impl SettingsGuard {
        fn with_openclaw_dir(path: &Path) -> Self {
            let previous = get_settings();
            let mut settings = AppSettings::default();
            settings.openclaw_config_dir = Some(path.display().to_string());
            update_settings(settings).expect("set openclaw override dir");
            Self { previous }
        }
    }

    impl Drop for SettingsGuard {
        fn drop(&mut self) {
            update_settings(self.previous.clone()).expect("restore previous settings");
        }
    }

    struct RuntimeCtxFixture {
        _temp_home: TempDir,
        _env: EnvGuard,
        terminal: TuiTerminal,
        app: App,
        data: UiData,
        proxy_loading: RequestTracker,
        webdav_loading: RequestTracker,
        update_check: RequestTracker,
    }

    fn runtime_ctx(app_type: AppType) -> RuntimeCtxFixture {
        let temp_home = TempDir::new().expect("create temp home");
        let env = EnvGuard::set_home(temp_home.path());

        let terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let app = App::new(Some(app_type.clone()));
        let data = UiData::load(&app_type).expect("load ui data");
        RuntimeCtxFixture {
            _temp_home: temp_home,
            _env: env,
            terminal,
            app,
            data,
            proxy_loading: RequestTracker::default(),
            webdav_loading: RequestTracker::default(),
            update_check: RequestTracker::default(),
        }
    }

    #[test]
    #[serial(home_settings)]
    fn submit_config_common_snippet_returns_to_form_without_view_overlay() {
        let mut fixture = runtime_ctx(AppType::Claude);

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        super::submit(
            &mut ctx,
            EditorSubmit::ConfigCommonSnippet {
                app_type: AppType::Claude,
                source: crate::cli::tui::app::CommonSnippetViewSource::ProviderForm,
            },
            r#"{"env":{"COMMON_FLAG":"1"}}"#.to_string(),
        )
        .expect("common snippet submit should succeed");

        assert!(ctx.app.editor.is_none());
        assert!(matches!(
            ctx.app.overlay,
            crate::cli::tui::app::Overlay::None
        ));
    }

    #[test]
    #[serial(home_settings)]
    fn format_common_snippet_updates_editor_buffer_without_saving() {
        let mut fixture = runtime_ctx(AppType::Claude);
        fixture.app.open_editor(
            "Common Snippet",
            crate::cli::tui::app::EditorKind::Json,
            r#"{"env":{"COMMON_FLAG":"1"}}"#,
            EditorSubmit::ConfigCommonSnippet {
                app_type: AppType::Claude,
                source: crate::cli::tui::app::CommonSnippetViewSource::Global,
            },
        );

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        super::format_common_snippet(&mut ctx, AppType::Claude).expect("format common snippet");

        let content = ctx.app.editor.as_ref().expect("editor remains open").text();
        assert!(content.contains("\n  \"env\": {"));
        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Success,
                ..
            })
        ));
        assert!(
            ctx.data.config.common_snippet.trim().is_empty(),
            "formatting should not persist the snippet before Ctrl+S"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn extract_common_snippet_updates_editor_buffer_without_saving() {
        let mut fixture = runtime_ctx(AppType::Claude);

        let mut form = crate::cli::tui::form::ProviderAddFormState::new(AppType::Claude);
        form.name.set("Provider One");
        form.claude_base_url.set("https://provider.example");
        form.claude_api_key.set("sk-provider");
        form.extra = json!({
            "settingsConfig": {
                "env": {
                    "ANTHROPIC_BASE_URL": "https://provider.example",
                    "ANTHROPIC_AUTH_TOKEN": "sk-provider",
                    "COMMON_FLAG": "1"
                }
            }
        });
        fixture.app.form = Some(FormState::ProviderAdd(form));
        fixture.app.open_editor(
            "Common Snippet",
            crate::cli::tui::app::EditorKind::Json,
            "{}",
            EditorSubmit::ConfigCommonSnippet {
                app_type: AppType::Claude,
                source: crate::cli::tui::app::CommonSnippetViewSource::ProviderForm,
            },
        );

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        super::extract_common_snippet_into_editor(&mut ctx, AppType::Claude)
            .expect("extract common snippet into editor");

        let content = ctx.app.editor.as_ref().expect("editor remains open").text();
        assert!(content.contains("COMMON_FLAG"));
        assert!(!content.contains("ANTHROPIC_BASE_URL"));
        assert!(!content.contains("ANTHROPIC_AUTH_TOKEN"));
        assert!(
            ctx.data.config.common_snippet.trim().is_empty(),
            "extracting into the editor should not persist before Ctrl+S"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_prompt_create_persists_prompt_and_refreshes_selection() {
        let mut fixture = runtime_ctx(AppType::Claude);

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_prompt_create(
            &mut ctx,
            "prompt-one".to_string(),
            "Prompt One".to_string(),
            Some("Demo description".to_string()),
            "hello".to_string(),
        )
        .expect("create prompt succeeds");

        let refreshed = UiData::load(&AppType::Claude).expect("reload ui data");
        assert!(
            refreshed
                .prompts
                .rows
                .iter()
                .any(|row| row.id == "prompt-one"
                    && row.prompt.name == "Prompt One"
                    && row.prompt.description.as_deref() == Some("Demo description")),
            "runtime create should persist the prompt"
        );
        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Success,
                ..
            })
        ));
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_add_generates_id_when_name_is_valid() {
        let mut fixture = runtime_ctx(AppType::Claude);

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_add(
            &mut ctx,
            r#"{"id":"","name":"Provider One","settingsConfig":{"env":{"ANTHROPIC_BASE_URL":"https://example.com"}}}"#
                .to_string(),
        )
        .expect("submit should succeed");

        let refreshed = UiData::load(&AppType::Claude).expect("reload ui data");
        assert!(
            refreshed
                .providers
                .rows
                .iter()
                .any(|row| row.id == "provider-one"),
            "runtime submit should auto-generate and persist an id"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_add_rejects_name_that_cannot_generate_id() {
        let mut fixture = runtime_ctx(AppType::Claude);

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_add(
            &mut ctx,
            r#"{"id":"","name":"!!!","settingsConfig":{"env":{"ANTHROPIC_BASE_URL":"https://example.com"}}}"#
                .to_string(),
        )
        .expect("submit should return without crashing");

        let refreshed = UiData::load(&AppType::Claude).expect("reload ui data");
        assert!(
            refreshed.providers.rows.is_empty(),
            "runtime submit should refuse names that still yield an empty id"
        );
        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Warning,
                ..
            })
        ));
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_add_rejects_blank_codex_base_url() {
        let mut fixture = runtime_ctx(AppType::Codex);

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_add(
            &mut ctx,
            r#"{
  "id": "",
  "name": "Codex Provider",
  "settingsConfig": {
    "auth": {
      "OPENAI_API_KEY": "sk-test"
    },
    "config": "model_provider = \"custom\"\nmodel = \"gpt-5.4\"\nmodel_reasoning_effort = \"high\"\ndisable_response_storage = true\n\n[model_providers.custom]\nname = \"custom\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n"
  }
}"#
            .to_string(),
        )
        .expect("submit should return without crashing");

        let refreshed = UiData::load(&AppType::Codex).expect("reload ui data");
        assert!(
            refreshed.providers.rows.is_empty(),
            "runtime submit should reject Codex providers without a base_url"
        );
        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Warning,
                message,
                ..
            }) if message == texts::base_url_empty_error()
        ));
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_add_preserves_custom_openclaw_name_after_reload() {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        let mut terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let mut app = App::new(Some(AppType::OpenClaw));
        let mut data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_add(
            &mut ctx,
            r#"{
  "id": "",
  "name": "Friendly OpenClaw",
  "settingsConfig": {
    "apiKey": "sk-friendly",
    "baseUrl": "https://friendly.example/v1",
    "models": [
      { "id": "friendly-model", "name": "Friendly Model" }
    ]
  }
}"#
            .to_string(),
        )
        .expect("submit should succeed");

        let refreshed = UiData::load(&AppType::OpenClaw).expect("reload openclaw ui data");
        let refreshed_row = refreshed
            .providers
            .rows
            .iter()
            .find(|row| row.id == "friendly-openclaw")
            .expect("provider row should still exist after reload");
        assert_eq!(refreshed_row.provider.name, "Friendly OpenClaw");
        assert!(
            refreshed_row.provider.created_at.is_some(),
            "adding an OpenClaw provider through the add flow should persist a user-touched marker"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_edit_updates_openclaw_live_backed_provider() {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(
            r#"{
  models: {
    mode: 'merge',
    providers: {
      'live-only': {
        apiKey: 'sk-live-old',
        baseUrl: 'https://live.old.example/v1',
        models: [{ id: 'live-model-old', name: 'Live Model Old' }],
      },
    },
  },
}"#,
        )
        .expect("seed live-only openclaw provider");

        let mut terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let mut app = App::new(Some(AppType::OpenClaw));
        let mut data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");
        assert!(
            data.providers
                .rows
                .iter()
                .any(|row| row.id == "live-only" && row.is_saved && row.is_in_config),
            "precondition: UI should mirror the live provider into the local manager before edit submit"
        );

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_edit(
            &mut ctx,
            "live-only".to_string(),
            r#"{
  "id": "live-only",
  "name": "Live Only Imported",
  "settingsConfig": {
    "apiKey": "sk-live-new",
    "baseUrl": "https://live.new.example/v1",
    "models": [
      { "id": "live-model-new", "name": "Live Model New" }
    ]
  }
}"#
            .to_string(),
        )
        .expect("submit should succeed");

        let refreshed = UiData::load(&AppType::OpenClaw).expect("reload openclaw ui data");
        assert!(
            refreshed
                .providers
                .rows
                .iter()
                .any(|row| row.id == "live-only" && row.is_saved && row.is_in_config),
            "editing an OpenClaw provider should keep the mirrored row in sync"
        );
        assert_eq!(
            crate::openclaw_config::get_provider("live-only")
                .expect("read rewritten live provider")
                .and_then(|provider| provider
                    .get("baseUrl")
                    .and_then(Value::as_str)
                    .map(str::to_string)),
            Some("https://live.new.example/v1".to_string())
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_edit_rejects_blank_codex_base_url() {
        let mut fixture = runtime_ctx(AppType::Codex);
        let state = load_state().expect("load state");
        {
            let mut config = state.config.write().expect("lock config");
            let manager = config
                .get_manager_mut(&AppType::Codex)
                .expect("codex manager");
            manager.providers.insert(
                "codex-provider".to_string(),
                Provider::with_id(
                    "codex-provider".to_string(),
                    "Codex Provider".to_string(),
                    json!({
                        "auth": { "OPENAI_API_KEY": "sk-test" },
                        "config": "model_provider = \"custom\"\nmodel = \"gpt-5.4\"\nmodel_reasoning_effort = \"high\"\ndisable_response_storage = true\n\n[model_providers.custom]\nname = \"custom\"\nbase_url = \"https://api.example.com/v1\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n"
                    }),
                    None,
                ),
            );
        }
        state.save().expect("persist codex provider");
        fixture.data = UiData::load(&AppType::Codex).expect("reload codex data");

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_edit(
            &mut ctx,
            "codex-provider".to_string(),
            r#"{
  "id": "codex-provider",
  "name": "Codex Provider",
  "settingsConfig": {
    "auth": {
      "OPENAI_API_KEY": "sk-test"
    },
    "config": "model_provider = \"custom\"\nmodel = \"gpt-5.4\"\nmodel_reasoning_effort = \"high\"\ndisable_response_storage = true\n\n[model_providers.custom]\nname = \"custom\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n"
  }
}"#
            .to_string(),
        )
        .expect("submit should return without crashing");

        let refreshed = UiData::load(&AppType::Codex).expect("reload ui data");
        let row = refreshed
            .providers
            .rows
            .iter()
            .find(|row| row.id == "codex-provider")
            .expect("provider should remain present");
        let config_text = row.provider.settings_config["config"]
            .as_str()
            .expect("codex config text should remain present");
        assert!(
            config_text.contains("base_url = \"https://api.example.com/v1\""),
            "failed edit should keep the existing base_url intact"
        );
        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Warning,
                message,
                ..
            }) if message == texts::base_url_empty_error()
        ));
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_edit_preserves_custom_openclaw_name_after_reload() {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(
            r#"{
  models: {
    mode: 'merge',
    providers: {
      'live-only': {
        apiKey: 'sk-live',
        baseUrl: 'https://live.example/v1',
        models: [{ id: 'live-model', name: 'Live Model' }],
      },
    },
  },
}"#,
        )
        .expect("seed live-only openclaw provider");

        let mut terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let mut app = App::new(Some(AppType::OpenClaw));
        let mut data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");
        let initial_row = data
            .providers
            .rows
            .iter()
            .find(|row| row.id == "live-only")
            .expect("mirrored provider row should exist before edit");
        assert_eq!(initial_row.provider.name, "live-only");
        assert!(initial_row.provider.created_at.is_none());

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_edit(
            &mut ctx,
            "live-only".to_string(),
            r#"{
  "id": "live-only",
  "name": "Live Only Custom",
  "meta": {
    "applyCommonConfig": false
  },
  "settingsConfig": {
    "apiKey": "sk-live",
    "baseUrl": "https://live.example/v1",
    "models": [
      { "id": "live-model", "name": "Live Model" }
    ]
  }
}"#
            .to_string(),
        )
        .expect("submit should succeed");

        let refreshed = UiData::load(&AppType::OpenClaw).expect("reload openclaw ui data");
        let refreshed_row = refreshed
            .providers
            .rows
            .iter()
            .find(|row| row.id == "live-only")
            .expect("provider row should still exist after reload");
        assert_eq!(refreshed_row.provider.name, "Live Only Custom");
        assert!(
            refreshed_row.provider.created_at.is_some(),
            "saving a mirrored OpenClaw provider through the edit flow should persist a user-touched marker"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_edit_keeps_saved_only_openclaw_snapshot_rows_visible() {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(
            r#"{
  models: {
    mode: 'merge',
    providers: {
      keep: {
        apiKey: 'sk-keep',
        baseUrl: 'https://keep.example/v1',
        models: [{ id: 'keep-model' }],
      },
    },
  },
  agents: {
    defaults: {
      model: {
        primary: 'keep/keep-model',
      },
    },
  },
}"#,
        )
        .expect("seed unrelated live openclaw provider");

        let state = load_state().expect("load state");
        {
            let mut config = state.config.write().expect("lock config");
            let manager = config
                .get_manager_mut(&AppType::OpenClaw)
                .expect("openclaw manager");
            manager.providers.insert(
                "saved-only".to_string(),
                Provider::with_id(
                    "saved-only".to_string(),
                    "Saved Only".to_string(),
                    json!({
                        "apiKey": "sk-saved-old",
                        "baseUrl": "https://saved.old.example/v1",
                        "models": [{ "id": "saved-model-old" }]
                    }),
                    None,
                ),
            );
        }
        state.save().expect("persist saved-only provider snapshot");

        let data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");
        let row = data
            .providers
            .rows
            .iter()
            .find(|row| row.id == "saved-only")
            .expect("saved-only snapshot rows should remain visible after OpenClaw reload");
        assert!(!row.is_in_config);
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_edit_rejects_invalid_usage_script_for_openclaw_provider() {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(
            r#"{
  models: {
    mode: 'merge',
    providers: {
      keep: {
        apiKey: 'sk-keep',
        baseUrl: 'https://keep.example/v1',
        models: [{ id: 'keep-model' }],
      },
    },
  },
}"#,
        )
        .expect("seed unrelated live openclaw provider");

        let state = load_state().expect("load state");
        {
            let mut config = state.config.write().expect("lock config");
            let manager = config
                .get_manager_mut(&AppType::OpenClaw)
                .expect("openclaw manager");
            manager.providers.insert(
                "keep".to_string(),
                Provider::with_id(
                    "keep".to_string(),
                    "Keep".to_string(),
                    json!({
                        "apiKey": "sk-keep",
                        "baseUrl": "https://keep.example/v1",
                        "models": [{ "id": "keep-model" }]
                    }),
                    None,
                ),
            );
        }
        state.save().expect("persist mirrored provider state");

        let original_live =
            std::fs::read_to_string(crate::openclaw_config::get_openclaw_config_path())
                .expect("read original live config");
        let mut terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let mut app = App::new(Some(AppType::OpenClaw));
        let mut data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");
        assert!(
            data.providers
                .rows
                .iter()
                .any(|row| row.id == "keep" && row.is_saved && row.is_in_config),
            "precondition: UI should expose the mirrored provider before edit submit"
        );

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_edit(
            &mut ctx,
            "keep".to_string(),
            r#"{
  "id": "keep",
  "name": "Keep Invalid",
  "settingsConfig": {
    "apiKey": "sk-keep-new",
    "baseUrl": "https://keep.new.example/v1",
    "models": [
      { "id": "keep-model-new" }
    ]
  },
  "meta": {
    "usage_script": {
      "enabled": true,
      "language": "javascript",
      "code": "return { success: true, data: [] };",
      "autoQueryInterval": 1441
    }
  }
}"#
            .to_string(),
        )
        .expect("submit should return without crashing");

        assert!(matches!(
            ctx.app.toast.as_ref(),
            Some(Toast {
                kind: ToastKind::Error,
                ..
            })
        ));
        assert!(
            ctx.app
                .toast
                .as_ref()
                .is_some_and(|toast| toast.message.contains("1440")),
            "OpenClaw edits should surface usage_script validation errors"
        );

        let refreshed = UiData::load(&AppType::OpenClaw).expect("reload openclaw ui data");
        let saved_row = refreshed
            .providers
            .rows
            .iter()
            .find(|row| row.id == "keep")
            .expect("provider row should still exist");
        assert_eq!(saved_row.provider.name, "Keep");
        assert_eq!(
            saved_row.provider.settings_config["baseUrl"],
            json!("https://keep.example/v1"),
            "invalid OpenClaw edits should not persist the attempted provider update"
        );
        let live_after =
            std::fs::read_to_string(crate::openclaw_config::get_openclaw_config_path())
                .expect("read live config after rejected saved-only edit");
        assert_eq!(
            live_after, original_live,
            "rejected OpenClaw edits should leave openclaw.json untouched"
        );
    }

    fn run_openclaw_editor_submit_flow(
        route: Route,
        open_key: KeyEvent,
        initial_source: &str,
        expected_submit: EditorSubmit,
        expected_initial_warning: &str,
        edited_content: &str,
    ) -> Result<(App, UiData), AppError> {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source)?;

        let mut terminal = TuiTerminal::new_for_test()?;
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = route;
        app.focus = Focus::Content;
        let mut data = UiData::load(&AppType::OpenClaw)?;
        assert!(data
            .config
            .openclaw_warnings
            .as_ref()
            .is_some_and(|warnings| warnings
                .iter()
                .any(|warning| warning.code == expected_initial_warning)));

        let open_action = app.on_key(open_key, &data);
        assert!(matches!(open_action, Action::None));
        assert!(matches!(
            app.editor.as_ref().map(|editor| editor.submit.clone()),
            Some(submit) if submit == expected_submit
        ));

        app.editor
            .as_mut()
            .expect("editor should be open")
            .replace_text(edited_content);

        let submit_action = app.on_key(ctrl(KeyCode::Char('s')), &data);
        let Action::EditorSubmit { submit, content } = submit_action else {
            panic!("expected EditorSubmit action");
        };

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, submit, content)?;
        Ok((app, data))
    }

    fn run_openclaw_tools_form_submit_flow(
        initial_source: &str,
        tools: &OpenClawToolsConfig,
    ) -> Result<(App, UiData), AppError> {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source)?;

        let mut terminal = TuiTerminal::new_for_test()?;
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = Route::ConfigOpenClawTools;
        app.focus = Focus::Content;
        app.openclaw_tools_form = Some(
            crate::cli::tui::app::OpenClawToolsFormState::from_snapshot(Some(tools)),
        );
        let mut data = UiData::load(&AppType::OpenClaw)?;

        let content =
            serde_json::to_string_pretty(tools).expect("serialize structured tools form content");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, EditorSubmit::ConfigOpenClawTools, content)?;
        Ok((app, data))
    }

    fn run_openclaw_tools_form_submit_flow_with_state(
        initial_source: &str,
        form: crate::cli::tui::app::OpenClawToolsFormState,
    ) -> Result<(App, UiData), AppError> {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source)?;

        let mut terminal = TuiTerminal::new_for_test()?;
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = Route::ConfigOpenClawTools;
        app.focus = Focus::Content;
        app.openclaw_tools_form = Some(form.clone());
        let mut data = UiData::load(&AppType::OpenClaw)?;

        let content = serde_json::to_string_pretty(&form.to_config())
            .expect("serialize structured tools form content");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, EditorSubmit::ConfigOpenClawTools, content)?;
        Ok((app, data))
    }

    fn run_openclaw_agents_form_submit_flow(
        initial_source: &str,
        defaults: &OpenClawAgentsDefaults,
    ) -> Result<(App, UiData), AppError> {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source)?;

        let mut terminal = TuiTerminal::new_for_test()?;
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = Route::ConfigOpenClawAgents;
        app.focus = Focus::Content;
        app.openclaw_agents_form =
            Some(crate::cli::tui::app::OpenClawAgentsFormState::from_snapshot(Some(defaults)));
        let mut data = UiData::load(&AppType::OpenClaw)?;

        let content = serde_json::to_string_pretty(defaults)
            .expect("serialize structured agents form content");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, EditorSubmit::ConfigOpenClawAgents, content)?;
        Ok((app, data))
    }

    fn run_openclaw_agents_form_submit_flow_with_state(
        initial_source: &str,
        form: crate::cli::tui::app::OpenClawAgentsFormState,
    ) -> Result<(App, UiData), AppError> {
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source)?;

        let mut terminal = TuiTerminal::new_for_test()?;
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = Route::ConfigOpenClawAgents;
        app.focus = Focus::Content;
        app.openclaw_agents_form = Some(form.clone());
        let mut data = UiData::load(&AppType::OpenClaw)?;

        let content = serde_json::to_string_pretty(&form.to_config())
            .expect("serialize structured agents form content");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, EditorSubmit::ConfigOpenClawAgents, content)?;
        Ok((app, data))
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_env_editor_submit_saves_and_reloads_ui_data() {
        let initial_source = r#"{
  env: {
    vars: 'broken-env',
  },
}"#;
        let edited_content = r#"{
  "OPENCLAW_ENV_TOKEN": "fresh-token",
  "OPENCLAW_TIMEOUT": 30
}"#;

        let (app, data) = run_openclaw_editor_submit_flow(
            Route::ConfigOpenClawEnv,
            key(KeyCode::Enter),
            initial_source,
            EditorSubmit::ConfigOpenClawEnv,
            "stringified_env_vars",
            edited_content,
        )
        .expect("env submit flow should succeed");

        assert!(app.editor.is_none());
        assert_eq!(
            app.toast.as_ref().map(|toast| toast.message.as_str()),
            Some(
                texts::tui_toast_openclaw_config_saved(texts::tui_config_item_openclaw_env())
                    .as_str()
            )
        );
        assert_eq!(
            data.config
                .openclaw_env
                .as_ref()
                .and_then(|env| env.vars.get("OPENCLAW_ENV_TOKEN")),
            Some(&Value::String("fresh-token".to_string()))
        );
        let warnings = data.config.openclaw_warnings.clone().unwrap_or_default();
        assert!(!warnings
            .iter()
            .any(|warning| warning.code == "stringified_env_vars"));
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_tools_form_submit_saves_and_reloads_ui_data() {
        let initial_source = r#"{
  tools: {
    profile: 'dangerous',
    allow: ['Read'],
  },
}"#;
        let edited_tools = OpenClawToolsConfig {
            profile: Some("coding".to_string()),
            allow: vec!["Read".to_string(), "Write".to_string()],
            deny: vec!["Bash".to_string()],
            extra: std::collections::HashMap::new(),
        };

        let (app, data) = run_openclaw_tools_form_submit_flow(initial_source, &edited_tools)
            .expect("tools submit flow should succeed");

        assert!(app.editor.is_none());
        let form = app
            .openclaw_tools_form
            .as_ref()
            .expect("tools form should be reloaded after save");
        assert_eq!(
            form.section,
            crate::cli::tui::app::OpenClawToolsSection::Profile
        );
        assert_eq!(form.row, 0);
        assert!(
            app.toast.is_none(),
            "successful tools auto-save should stay silent"
        );
        assert_eq!(
            data.config
                .openclaw_tools
                .as_ref()
                .and_then(|tools| tools.profile.as_deref()),
            Some("coding")
        );
        assert_eq!(
            data.config
                .openclaw_tools
                .as_ref()
                .map(|tools| tools.allow.clone()),
            Some(vec!["Read".to_string(), "Write".to_string()])
        );
        let warnings = data.config.openclaw_warnings.clone().unwrap_or_default();
        assert!(!warnings
            .iter()
            .any(|warning| warning.code == "invalid_tools_profile"));
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_tools_form_submit_preserves_structured_focus_after_reload() {
        let initial_source = r#"{
  tools: {
    profile: 'dangerous',
    allow: ['Read'],
  },
}"#;
        let mut form = crate::cli::tui::app::OpenClawToolsFormState::from_snapshot(Some(
            &OpenClawToolsConfig {
                profile: Some("coding".to_string()),
                allow: vec!["Read".to_string(), "Write".to_string()],
                deny: vec!["Bash".to_string(), "Exec".to_string()],
                extra: HashMap::new(),
            },
        ));
        form.section = crate::cli::tui::app::OpenClawToolsSection::Deny;
        form.row = 1;

        let (app, data) = run_openclaw_tools_form_submit_flow_with_state(initial_source, form)
            .expect("tools submit flow should succeed");

        let form = app
            .openclaw_tools_form
            .as_ref()
            .expect("tools form should stay available after reload");
        assert_eq!(
            form.section,
            crate::cli::tui::app::OpenClawToolsSection::Deny
        );
        assert_eq!(form.row, 1);
        assert_eq!(form.deny, vec!["Bash".to_string(), "Exec".to_string()]);
        assert_eq!(
            data.config
                .openclaw_tools
                .as_ref()
                .map(|tools| tools.deny.clone()),
            Some(vec!["Bash".to_string(), "Exec".to_string()])
        );
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_agents_form_submit_saves_and_reloads_ui_data() {
        let initial_source = r#"{
  models: {
    mode: 'merge',
    providers: {
      demo: {
        models: [
          { id: 'gpt-4.1' },
          { id: 'gpt-4o-mini' },
        ],
      },
    },
  },
  agents: {
    defaults: {
      timeout: 42,
      model: {
        primary: 'legacy-model',
      },
    },
  },
}"#;
        let edited_defaults = OpenClawAgentsDefaults {
            model: Some(crate::openclaw_config::OpenClawDefaultModel {
                primary: "demo/gpt-4.1".to_string(),
                fallbacks: vec!["demo/gpt-4o-mini".to_string()],
                extra: HashMap::new(),
            }),
            models: Some(HashMap::from([(
                "demo/gpt-4.1".to_string(),
                OpenClawModelCatalogEntry {
                    alias: Some("General".to_string()),
                    extra: HashMap::new(),
                },
            )])),
            extra: HashMap::new(),
        };

        let (app, data) = run_openclaw_agents_form_submit_flow(initial_source, &edited_defaults)
            .expect("agents submit flow should succeed");

        assert!(app.editor.is_none());
        let form = app
            .openclaw_agents_form
            .as_ref()
            .expect("agents form should be reloaded after save");
        assert_eq!(
            form.section,
            crate::cli::tui::app::OpenClawAgentsSection::PrimaryModel
        );
        assert_eq!(form.row, 0);
        assert!(
            app.toast.is_none(),
            "successful agents auto-save should stay silent"
        );
        assert_eq!(
            data.config
                .openclaw_agents_defaults
                .as_ref()
                .and_then(|defaults| defaults.model.as_ref())
                .map(|model| model.primary.as_str()),
            Some("demo/gpt-4.1")
        );
        assert_eq!(
            data.config
                .openclaw_agents_defaults
                .as_ref()
                .and_then(|defaults| defaults.models.as_ref())
                .and_then(|models| models.get("demo/gpt-4.1")),
            Some(&OpenClawModelCatalogEntry {
                alias: Some("General".to_string()),
                extra: HashMap::new(),
            })
        );
        let warnings = data.config.openclaw_warnings.clone().unwrap_or_default();
        assert!(!warnings
            .iter()
            .any(|warning| warning.code == "legacy_agents_timeout"));
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_agents_form_submit_preserves_structured_focus_after_reload() {
        let initial_source = r#"{
  models: {
    mode: 'merge',
    providers: {
      demo: {
        models: [
          { id: 'gpt-4.1' },
          { id: 'gpt-4o-mini' },
        ],
      },
    },
  },
  agents: {
    defaults: {
      workspace: './workspace',
      contextTokens: 4096,
      maxConcurrent: 3,
      model: {
        primary: 'demo/gpt-4.1',
      },
    },
  },
}"#;
        let mut form = crate::cli::tui::app::OpenClawAgentsFormState::from_snapshot(Some(
            &OpenClawAgentsDefaults {
                model: Some(crate::openclaw_config::OpenClawDefaultModel {
                    primary: "demo/gpt-4.1".to_string(),
                    fallbacks: vec!["demo/gpt-4o-mini".to_string()],
                    extra: HashMap::new(),
                }),
                models: None,
                extra: HashMap::from([
                    (
                        "workspace".to_string(),
                        Value::String("./workspace".to_string()),
                    ),
                    ("contextTokens".to_string(), Value::from(4096)),
                    ("maxConcurrent".to_string(), Value::from(3)),
                ]),
            },
        ));
        form.section = crate::cli::tui::app::OpenClawAgentsSection::Runtime;
        form.row = 2;

        let (app, data) = run_openclaw_agents_form_submit_flow_with_state(initial_source, form)
            .expect("agents submit flow should succeed");

        let form = app
            .openclaw_agents_form
            .as_ref()
            .expect("agents form should stay available after reload");
        assert_eq!(
            form.section,
            crate::cli::tui::app::OpenClawAgentsSection::Runtime
        );
        assert_eq!(form.row, 2);
        assert_eq!(form.context_tokens, "4096");
        assert_eq!(
            data.config
                .openclaw_agents_defaults
                .as_ref()
                .and_then(|defaults| defaults.extra.get("contextTokens")),
            Some(&Value::from(4096))
        );
    }

    #[test]
    #[serial(home_settings)]
    fn openclaw_config_route_agents_form_submit_clears_stale_error_toast_after_success() {
        let initial_source = r#"{
  agents: {
    defaults: {
      workspace: './workspace',
    },
  },
}"#;
        let home_dir = tempdir().expect("create temp home");
        let openclaw_dir = tempdir().expect("create temp openclaw dir");
        let _home = EnvGuard::set_home(home_dir.path());
        let _settings = SettingsGuard::with_openclaw_dir(openclaw_dir.path());

        write_openclaw_config_source(initial_source).expect("seed openclaw config");

        let mut terminal = TuiTerminal::new_for_test().expect("create test terminal");
        let mut app = App::new(Some(AppType::OpenClaw));
        app.route = Route::ConfigOpenClawAgents;
        app.focus = Focus::Content;
        app.openclaw_agents_form = Some(
            crate::cli::tui::app::OpenClawAgentsFormState::from_snapshot(Some(
                &OpenClawAgentsDefaults {
                    model: None,
                    models: None,
                    extra: HashMap::from([(
                        "workspace".to_string(),
                        Value::String("./workspace-next".to_string()),
                    )]),
                },
            )),
        );
        app.push_toast(
            texts::tui_toast_openclaw_agents_save_failed_detail("previous failure"),
            ToastKind::Error,
        );
        let mut data = UiData::load(&AppType::OpenClaw).expect("load openclaw ui data");

        let content = serde_json::to_string_pretty(
            &app.openclaw_agents_form
                .as_ref()
                .expect("agents form should be seeded")
                .to_config(),
        )
        .expect("serialize structured agents form content");

        let mut proxy_loading = RequestTracker::default();
        let mut webdav_loading = RequestTracker::default();
        let mut update_check = RequestTracker::default();
        let mut ctx = RuntimeActionContext {
            terminal: &mut terminal,
            app: &mut app,
            data: &mut data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut webdav_loading,
            update_req_tx: None,
            update_check: &mut update_check,
            model_fetch_req_tx: None,
        };

        super::submit(&mut ctx, EditorSubmit::ConfigOpenClawAgents, content)
            .expect("agents submit flow should succeed");

        assert!(
            app.toast.is_none(),
            "successful agents auto-save should clear stale error toast without showing success"
        );
        assert_eq!(
            data.config
                .openclaw_agents_defaults
                .as_ref()
                .and_then(|defaults| defaults.extra.get("workspace")),
            Some(&Value::String("./workspace-next".to_string()))
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_form_apply_json_keeps_common_snippet_out_of_raw_submit_payload() {
        let mut fixture = runtime_ctx(AppType::Claude);

        fixture.data.config.common_snippet = r#"{
            "alwaysThinkingEnabled": false,
            "env": {
                "COMMON_FLAG": "1"
            }
        }"#
        .to_string();

        let mut form = crate::cli::tui::form::ProviderAddFormState::new(AppType::Claude);
        form.id.set("p1");
        form.name.set("Provider One");
        form.include_common_config = true;
        form.claude_base_url.set("https://provider.example");
        fixture.app.form = Some(FormState::ProviderAdd(form));

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_form_apply_json(
            &mut ctx,
            r#"{
                "alwaysThinkingEnabled": false,
                "env": {
                    "ANTHROPIC_BASE_URL": "https://edited.example",
                    "COMMON_FLAG": "1",
                    "EXTRA_FIELD": "kept"
                }
            }"#
            .to_string(),
        )
        .expect("apply should succeed");

        let FormState::ProviderAdd(form) = ctx
            .app
            .form
            .as_ref()
            .expect("provider form should remain open")
        else {
            panic!("expected provider form");
        };
        let settings = form
            .to_provider_json_value()
            .get("settingsConfig")
            .cloned()
            .expect("settingsConfig should exist");

        assert!(
            settings.get("alwaysThinkingEnabled").is_none(),
            "applying preview JSON should not persist top-level common snippet keys into raw form payload"
        );
        assert!(
            settings["env"].get("COMMON_FLAG").is_none(),
            "applying preview JSON should not persist nested common snippet keys into raw form payload"
        );
        assert_eq!(
            settings["env"]["ANTHROPIC_BASE_URL"], "https://edited.example",
            "provider-specific edits from the preview editor should still be preserved"
        );
        assert_eq!(
            settings["env"]["EXTRA_FIELD"], "kept",
            "non-common keys introduced in the preview editor should still be preserved"
        );
    }

    #[test]
    #[serial(home_settings)]
    fn submit_provider_form_apply_json_preserves_missing_meta_subset_detection() {
        let mut fixture = runtime_ctx(AppType::Claude);

        fixture.data.config.common_snippet = r#"{
            "alwaysThinkingEnabled": false,
            "env": {
                "COMMON_FLAG": "1"
            }
        }"#
        .to_string();

        let provider = Provider::with_id(
            "legacy-provider".to_string(),
            "Legacy Provider".to_string(),
            json!({
                "alwaysThinkingEnabled": false,
                "env": {
                    "ANTHROPIC_BASE_URL": "https://provider.example",
                    "COMMON_FLAG": "1"
                }
            }),
            None,
        );
        fixture.app.form = Some(FormState::ProviderAdd(
            crate::cli::tui::form::ProviderAddFormState::from_provider(AppType::Claude, &provider),
        ));

        let mut ctx = RuntimeActionContext {
            terminal: &mut fixture.terminal,
            app: &mut fixture.app,
            data: &mut fixture.data,
            speedtest_req_tx: None,
            stream_check_req_tx: None,
            skills_req_tx: None,
            proxy_req_tx: None,
            proxy_loading: &mut fixture.proxy_loading,
            local_env_req_tx: None,
            webdav_req_tx: None,
            webdav_loading: &mut fixture.webdav_loading,
            update_req_tx: None,
            update_check: &mut fixture.update_check,
            model_fetch_req_tx: None,
        };

        submit_provider_form_apply_json(
            &mut ctx,
            r#"{
                "alwaysThinkingEnabled": false,
                "env": {
                    "ANTHROPIC_BASE_URL": "https://edited.example",
                    "COMMON_FLAG": "1"
                }
            }"#
            .to_string(),
        )
        .expect("apply should succeed");

        let FormState::ProviderAdd(form) = ctx
            .app
            .form
            .as_ref()
            .expect("provider form should remain open")
        else {
            panic!("expected provider form");
        };
        let raw = form.to_provider_json_value();
        assert!(
            raw.get("meta")
                .and_then(|meta| meta.get("commonConfigEnabled"))
                .is_none(),
            "settings JSON edits on a missing-meta provider must not synthesize explicit common config metadata"
        );
        assert_eq!(
            raw["settingsConfig"]["alwaysThinkingEnabled"], false,
            "missing-meta providers must keep the common subset when metadata remains absent"
        );
        assert_eq!(
            raw["settingsConfig"]["env"]["COMMON_FLAG"], "1",
            "missing-meta providers need the common subset for backend subset detection"
        );
        assert_eq!(
            raw["settingsConfig"]["env"]["ANTHROPIC_BASE_URL"], "https://edited.example",
            "provider-specific edits from the settings JSON editor should still be preserved"
        );
    }
}
