use super::*;
use codex_config::types::AppToolApproval;
use codex_config::types::McpServerOAuthConfig;
use codex_config::types::McpServerToolConfig;
use codex_config::types::McpServerTransportConfig;
use codex_config::types::SessionPickerViewMode;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use tempfile::tempdir;
use toml::Value as TomlValue;

#[test]
fn blocking_set_model_top_level() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: Some("gpt-5.4".to_string()),
            effort: Some(ReasoningEffort::High),
        }],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"model = "gpt-5.4"
model_reasoning_effort = "high"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn set_service_tier_saves_default_as_default() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .set_service_tier(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, "service_tier = \"default\"\n");
}

#[test]
fn set_service_tier_saves_priority_as_fast() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .set_service_tier(Some(ServiceTier::Fast.request_value().to_string()))
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, "service_tier = \"fast\"\n");
}

#[test]
fn set_service_tier_preserves_unknown_service_tier() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .set_service_tier(Some("experimental-tier-id".to_string()))
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, "service_tier = \"experimental-tier-id\"\n");
}

#[test]
fn builder_with_edits_applies_custom_paths() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits(vec![ConfigEdit::SetPath {
            segments: vec!["enabled".to_string()],
            value: value(true),
        }])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, "enabled = true\n");
}

#[test]
fn session_picker_view_edit_writes_root_tui_setting() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([session_picker_view_edit(SessionPickerViewMode::Dense)])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[tui]
session_picker_view = "dense"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn keymap_binding_edit_writes_root_action_binding() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([keymap_binding_edit("composer", "submit", "ctrl-enter")])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[tui.keymap.composer]
submit = "ctrl-enter"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn keymap_bindings_edit_writes_single_binding_as_string() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([keymap_bindings_edit(
            "composer",
            "submit",
            &["ctrl-enter".to_string()],
        )])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[tui.keymap.composer]
submit = "ctrl-enter"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn keymap_bindings_edit_writes_multiple_bindings_as_array() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([keymap_bindings_edit(
            "composer",
            "submit",
            &["enter".to_string(), "ctrl-enter".to_string()],
        )])
        .apply_blocking()
        .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let value: TomlValue = toml::from_str(&raw).expect("parse config");

    assert_eq!(
        value
            .get("tui")
            .and_then(|value| value.get("keymap"))
            .and_then(|value| value.get("composer"))
            .and_then(|value| value.get("submit"))
            .and_then(TomlValue::as_array)
            .map(|values| {
                values
                    .iter()
                    .filter_map(TomlValue::as_str)
                    .collect::<Vec<_>>()
            }),
        Some(vec!["enter", "ctrl-enter"])
    );
}

#[test]
fn keymap_binding_edit_replaces_existing_binding_without_touching_profile() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"profile = "team"

[tui.keymap.composer]
submit = "enter"

[profiles.team.tui.keymap.composer]
submit = "shift-enter"
"#,
    )
    .expect("seed config");

    ConfigEditsBuilder::new(codex_home)
        .with_edits([keymap_binding_edit("composer", "submit", "ctrl-enter")])
        .apply_blocking()
        .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let value: TomlValue = toml::from_str(&raw).expect("parse config");

    assert_eq!(
        value
            .get("tui")
            .and_then(|value| value.get("keymap"))
            .and_then(|value| value.get("composer"))
            .and_then(|value| value.get("submit"))
            .and_then(TomlValue::as_str),
        Some("ctrl-enter")
    );
    assert_eq!(
        value
            .get("profiles")
            .and_then(|value| value.get("team"))
            .and_then(|value| value.get("tui"))
            .and_then(|value| value.get("keymap"))
            .and_then(|value| value.get("composer"))
            .and_then(|value| value.get("submit"))
            .and_then(TomlValue::as_str),
        Some("shift-enter")
    );
}

#[test]
fn keymap_binding_clear_edit_removes_root_action_binding_without_touching_profile() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"profile = "team"

[tui.keymap.composer]
submit = "enter"

[profiles.team.tui.keymap.composer]
submit = "shift-enter"
"#,
    )
    .expect("seed config");

    ConfigEditsBuilder::new(codex_home)
        .with_edits([keymap_binding_clear_edit("composer", "submit")])
        .apply_blocking()
        .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let value: TomlValue = toml::from_str(&raw).expect("parse config");

    assert_eq!(
        value
            .get("tui")
            .and_then(|value| value.get("keymap"))
            .and_then(|value| value.get("composer"))
            .and_then(|value| value.get("submit")),
        None
    );
    assert_eq!(
        value
            .get("profiles")
            .and_then(|value| value.get("team"))
            .and_then(|value| value.get("tui"))
            .and_then(|value| value.get("keymap"))
            .and_then(|value| value.get("composer"))
            .and_then(|value| value.get("submit"))
            .and_then(TomlValue::as_str),
        Some("shift-enter")
    );
}

#[test]
fn set_model_availability_nux_count_writes_shown_count() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    let shown_count = HashMap::from([("gpt-foo".to_string(), 4)]);

    ConfigEditsBuilder::new(codex_home)
        .set_model_availability_nux_count(&shown_count)
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[tui.model_availability_nux]
gpt-foo = 4
"#;
    assert_eq!(contents, expected);
}

#[test]
fn set_skill_config_writes_disabled_entry() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([ConfigEdit::SetSkillConfig {
            path: PathBuf::from("/tmp/skills/demo/SKILL.md"),
            enabled: false,
        }])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[[skills.config]]
path = "/tmp/skills/demo/SKILL.md"
enabled = false
"#;
    assert_eq!(contents, expected);
}

#[test]
fn set_skill_config_removes_entry_when_enabled() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[[skills.config]]
path = "/tmp/skills/demo/SKILL.md"
enabled = false
"#,
    )
    .expect("seed config");

    ConfigEditsBuilder::new(codex_home)
        .with_edits([ConfigEdit::SetSkillConfig {
            path: PathBuf::from("/tmp/skills/demo/SKILL.md"),
            enabled: true,
        }])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, "");
}

#[test]
fn set_skill_config_writes_name_selector_entry() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .with_edits([ConfigEdit::SetSkillConfigByName {
            name: "github:yeet".to_string(),
            enabled: false,
        }])
        .apply_blocking()
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[[skills.config]]
name = "github:yeet"
enabled = false
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_model_ignores_inline_legacy_profile_contents() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    // Seed with inline tables for profiles to simulate common user config.
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"profile = "fast"

profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
"#,
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: Some("o4-mini".to_string()),
            effort: None,
        }],
    )
    .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let value: TomlValue = toml::from_str(&raw).expect("parse config");

    assert_eq!(
        value.get("model").and_then(TomlValue::as_str),
        Some("o4-mini")
    );

    // Legacy profile values stay untouched when root settings are updated.
    let profiles_tbl = value
        .get("profiles")
        .and_then(|v| v.as_table())
        .expect("profiles table");
    let fast_tbl = profiles_tbl
        .get("fast")
        .and_then(|v| v.as_table())
        .expect("fast table");
    assert_eq!(
        fast_tbl.get("sandbox_mode").and_then(|v| v.as_str()),
        Some("strict")
    );
    assert_eq!(
        fast_tbl.get("model").and_then(|v| v.as_str()),
        Some("gpt-4o")
    );
}

#[cfg(unix)]
#[test]
fn blocking_set_model_writes_through_symlink_chain() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    let target_dir = tempdir().expect("target dir");
    let target_path = target_dir.path().join(CONFIG_TOML_FILE);
    let link_path = codex_home.join("config-link.toml");
    let config_path = codex_home.join(CONFIG_TOML_FILE);

    symlink(&target_path, &link_path).expect("symlink link");
    symlink("config-link.toml", &config_path).expect("symlink config");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: Some("gpt-5.4".to_string()),
            effort: Some(ReasoningEffort::High),
        }],
    )
    .expect("persist");

    let meta = std::fs::symlink_metadata(&config_path).expect("config metadata");
    assert!(meta.file_type().is_symlink());

    let contents = std::fs::read_to_string(&target_path).expect("read target");
    let expected = r#"model = "gpt-5.4"
model_reasoning_effort = "high"
"#;
    assert_eq!(contents, expected);
}

#[cfg(unix)]
#[test]
fn blocking_set_model_replaces_symlink_on_cycle() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    let link_a = codex_home.join("a.toml");
    let link_b = codex_home.join("b.toml");
    let config_path = codex_home.join(CONFIG_TOML_FILE);

    symlink("b.toml", &link_a).expect("symlink a");
    symlink("a.toml", &link_b).expect("symlink b");
    symlink("a.toml", &config_path).expect("symlink config");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: Some("gpt-5.4".to_string()),
            effort: None,
        }],
    )
    .expect("persist");

    let meta = std::fs::symlink_metadata(&config_path).expect("config metadata");
    assert!(!meta.file_type().is_symlink());

    let contents = std::fs::read_to_string(&config_path).expect("read config");
    let expected = r#"model = "gpt-5.4"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn batch_write_table_upsert_preserves_inline_comments() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    let original = r#"approval_policy = "never"

[mcp_servers.linear]
name = "linear"
# ok
url = "https://linear.example"

[mcp_servers.linear.http_headers]
foo = "bar"

[sandbox_workspace_write]
# ok 3
network_access = false
"#;
    std::fs::write(codex_home.join(CONFIG_TOML_FILE), original).expect("seed config");

    apply_blocking(
        codex_home,
        &[
            ConfigEdit::SetPath {
                segments: vec![
                    "mcp_servers".to_string(),
                    "linear".to_string(),
                    "url".to_string(),
                ],
                value: value("https://linear.example/v2"),
            },
            ConfigEdit::SetPath {
                segments: vec![
                    "sandbox_workspace_write".to_string(),
                    "network_access".to_string(),
                ],
                value: value(true),
            },
        ],
    )
    .expect("apply");

    let updated = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"approval_policy = "never"

[mcp_servers.linear]
name = "linear"
# ok
url = "https://linear.example/v2"

[mcp_servers.linear.http_headers]
foo = "bar"

[sandbox_workspace_write]
# ok 3
network_access = true
"#;
    assert_eq!(updated, expected);
}

#[test]
fn blocking_clear_model_does_not_follow_legacy_active_profile() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"profile = "fast"

profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
"#,
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: None,
            effort: Some(ReasoningEffort::High),
        }],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"profile = "fast"

profiles = { fast = { model = "gpt-4o", sandbox_mode = "strict" } }
model_reasoning_effort = "high"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_model_does_not_follow_legacy_active_profile() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"profile = "team"

[profiles.team]
model_reasoning_effort = "low"
"#,
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetModel {
            model: Some("o5-preview".to_string()),
            effort: Some(ReasoningEffort::Minimal),
        }],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"profile = "team"
model = "o5-preview"
model_reasoning_effort = "minimal"

[profiles.team]
model_reasoning_effort = "low"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_full_access_warning_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"# Global comment

[notice]
# keep me
existing = "value"
"#,
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeHideFullAccessWarning(true)],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"# Global comment

[notice]
# keep me
existing = "value"
hide_full_access_warning = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_rate_limit_model_nudge_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"
hide_rate_limit_model_nudge = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_gpt5_1_migration_prompt_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeHideModelMigrationPrompt(
            "hide_gpt5_1_migration_prompt".to_string(),
            true,
        )],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"
hide_gpt5_1_migration_prompt = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_gpt_5_1_codex_max_migration_prompt_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeHideModelMigrationPrompt(
            "hide_gpt-5.1-codex-max_migration_prompt".to_string(),
            true,
        )],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"
"hide_gpt-5.1-codex-max_migration_prompt" = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_record_model_migration_seen_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[ConfigEdit::RecordModelMigrationSeen {
            from: "gpt-5.2".to_string(),
            to: "gpt-5.4".to_string(),
        }],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"

[notice.model_migrations]
"gpt-5.2" = "gpt-5.4"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_external_config_migration_prompt_home_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(
            true,
        )],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"

[notice.external_config_migration_prompts]
home = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_hide_external_config_migration_prompt_project_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[
            ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
                "/Users/alexsong/code/skills".to_string(),
                true,
            ),
        ],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"

[notice.external_config_migration_prompts.projects]
"/Users/alexsong/code/skills" = true
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_external_config_migration_prompt_home_last_prompted_at_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"

[notice.external_config_migration_prompts]
home_last_prompted_at = 1760000000
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_set_external_config_migration_prompt_project_last_prompted_at_preserves_table() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[notice]
existing = "value"
"#,
    )
    .expect("seed");
    apply_blocking(
        codex_home,
        &[
            ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(
                "/Users/alexsong/code/skills".to_string(),
                1_760_000_000,
            ),
        ],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[notice]
existing = "value"

[notice.external_config_migration_prompts.project_last_prompted_at]
"/Users/alexsong/code/skills" = 1760000000
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_replace_mcp_servers_round_trips() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    let mut servers = BTreeMap::new();
    servers.insert(
        "stdio".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "cmd".to_string(),
                args: vec!["--flag".to_string()],
                env: Some(
                    [
                        ("B".to_string(), "2".to_string()),
                        ("A".to_string(), "1".to_string()),
                    ]
                    .into_iter()
                    .collect(),
                ),
                env_vars: vec!["FOO".into()],
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: true,
            required: false,
            supports_parallel_tool_calls: true,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::new(),
        },
    );

    servers.insert(
        "http".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::StreamableHttp {
                url: "https://example.com".to_string(),
                bearer_token_env_var: Some("TOKEN".to_string()),
                http_headers: Some(
                    [("Z-Header".to_string(), "z".to_string())]
                        .into_iter()
                        .collect(),
                ),
                env_http_headers: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: false,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: Some(std::time::Duration::from_secs(5)),
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: None,
            disabled_tools: Some(vec!["forbidden".to_string()]),
            scopes: None,
            oauth: Some(McpServerOAuthConfig {
                client_id: Some("eci-prd-pub-codex-123".to_string()),
            }),
            oauth_resource: Some("https://resource.example.com".to_string()),
            tools: HashMap::new(),
        },
    );

    apply_blocking(
        codex_home,
        &[ConfigEdit::ReplaceMcpServers(servers.clone())],
    )
    .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = "\
[mcp_servers.http]
url = \"https://example.com\"
bearer_token_env_var = \"TOKEN\"
enabled = false
startup_timeout_sec = 5.0
disabled_tools = [\"forbidden\"]
oauth_resource = \"https://resource.example.com\"

[mcp_servers.http.http_headers]
Z-Header = \"z\"

[mcp_servers.http.oauth]
client_id = \"eci-prd-pub-codex-123\"

[mcp_servers.stdio]
command = \"cmd\"
args = [\"--flag\"]
env_vars = [\"FOO\"]
supports_parallel_tool_calls = true
enabled_tools = [\"one\", \"two\"]

[mcp_servers.stdio.env]
A = \"1\"
B = \"2\"
";
    assert_eq!(raw, expected);
}

#[test]
fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    let mut servers = BTreeMap::new();
    servers.insert(
        "docs".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "docs-server".to_string(),
                args: Vec::new(),
                env: None,
                env_vars: Vec::new(),
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: true,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: Some(AppToolApproval::Prompt),
            enabled_tools: None,
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::from([(
                "search".to_string(),
                McpServerToolConfig {
                    approval_mode: Some(AppToolApproval::Approve),
                },
            )]),
        },
    );

    apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = "\
[mcp_servers.docs]
command = \"docs-server\"
default_tools_approval_mode = \"prompt\"

[mcp_servers.docs.tools.search]
approval_mode = \"approve\"
";
    assert_eq!(raw, expected);
}

#[test]
fn blocking_replace_mcp_servers_preserves_inline_comments() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[mcp_servers]
# keep me
foo = { command = "cmd" }
"#,
    )
    .expect("seed");

    let mut servers = BTreeMap::new();
    servers.insert(
        "foo".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "cmd".to_string(),
                args: Vec::new(),
                env: None,
                env_vars: Vec::new(),
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: true,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: None,
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::new(),
        },
    );

    apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[mcp_servers]
# keep me
foo = { command = "cmd" }
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_replace_mcp_servers_preserves_inline_comment_suffix() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[mcp_servers]
foo = { command = "cmd" } # keep me
"#,
    )
    .expect("seed");

    let mut servers = BTreeMap::new();
    servers.insert(
        "foo".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "cmd".to_string(),
                args: Vec::new(),
                env: None,
                env_vars: Vec::new(),
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: false,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: None,
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::new(),
        },
    );

    apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[mcp_servers]
foo = { command = "cmd" , enabled = false } # keep me
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_replace_mcp_servers_preserves_inline_comment_after_removing_keys() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[mcp_servers]
foo = { command = "cmd", args = ["--flag"] } # keep me
"#,
    )
    .expect("seed");

    let mut servers = BTreeMap::new();
    servers.insert(
        "foo".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "cmd".to_string(),
                args: Vec::new(),
                env: None,
                env_vars: Vec::new(),
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: true,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: None,
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::new(),
        },
    );

    apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[mcp_servers]
foo = { command = "cmd"} # keep me
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_replace_mcp_servers_preserves_inline_comment_prefix_on_update() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        r#"[mcp_servers]
# keep me
foo = { command = "cmd" }
"#,
    )
    .expect("seed");

    let mut servers = BTreeMap::new();
    servers.insert(
        "foo".to_string(),
        McpServerConfig {
            transport: McpServerTransportConfig::Stdio {
                command: "cmd".to_string(),
                args: Vec::new(),
                env: None,
                env_vars: Vec::new(),
                cwd: None,
            },
            environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
            enabled: false,
            required: false,
            supports_parallel_tool_calls: false,
            disabled_reason: None,
            startup_timeout_sec: None,
            tool_timeout_sec: None,
            default_tools_approval_mode: None,
            enabled_tools: None,
            disabled_tools: None,
            scopes: None,
            oauth: None,
            oauth_resource: None,
            tools: HashMap::new(),
        },
    );

    apply_blocking(codex_home, &[ConfigEdit::ReplaceMcpServers(servers)]).expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"[mcp_servers]
# keep me
foo = { command = "cmd" , enabled = false }
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_clear_path_noop_when_missing() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    apply_blocking(
        codex_home,
        &[ConfigEdit::ClearPath {
            segments: vec!["missing".to_string()],
        }],
    )
    .expect("apply");

    assert!(
        !codex_home.join(CONFIG_TOML_FILE).exists(),
        "config.toml should not be created on noop"
    );
}

#[test]
fn blocking_set_path_updates_notifications() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    let item = value(false);
    apply_blocking(
        codex_home,
        &[ConfigEdit::SetPath {
            segments: vec!["tui".to_string(), "notifications".to_string()],
            value: item,
        }],
    )
    .expect("apply");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let config: TomlValue = toml::from_str(&raw).expect("parse config");
    let notifications = config
        .get("tui")
        .and_then(|item| item.as_table())
        .and_then(|tbl| tbl.get("notifications"))
        .and_then(toml::Value::as_bool);
    assert_eq!(notifications, Some(false));
}

#[tokio::test]
async fn async_builder_set_model_persists() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path().to_path_buf();

    ConfigEditsBuilder::new(&codex_home)
        .set_model(Some("gpt-5.4"), Some(ReasoningEffort::High))
        .apply()
        .await
        .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let expected = r#"model = "gpt-5.4"
model_reasoning_effort = "high"
"#;
    assert_eq!(contents, expected);
}

#[test]
fn blocking_builder_set_model_round_trips_back_and_forth() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    let initial_expected = r#"model = "o4-mini"
model_reasoning_effort = "low"
"#;
    ConfigEditsBuilder::new(codex_home)
        .set_model(Some("o4-mini"), Some(ReasoningEffort::Low))
        .apply_blocking()
        .expect("persist initial");
    let mut contents =
        std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, initial_expected);

    let updated_expected = r#"model = "gpt-5.4"
model_reasoning_effort = "high"
"#;
    ConfigEditsBuilder::new(codex_home)
        .set_model(Some("gpt-5.4"), Some(ReasoningEffort::High))
        .apply_blocking()
        .expect("persist update");
    contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, updated_expected);

    ConfigEditsBuilder::new(codex_home)
        .set_model(Some("o4-mini"), Some(ReasoningEffort::Low))
        .apply_blocking()
        .expect("persist revert");
    contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert_eq!(contents, initial_expected);
}

#[tokio::test]
async fn blocking_set_asynchronous_helpers_available() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path().to_path_buf();

    ConfigEditsBuilder::new(&codex_home)
        .set_hide_full_access_warning(/*acknowledged*/ true)
        .apply()
        .await
        .expect("persist");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let notice = toml::from_str::<TomlValue>(&raw)
        .expect("parse config")
        .get("notice")
        .and_then(|item| item.as_table())
        .and_then(|tbl| tbl.get("hide_full_access_warning"))
        .and_then(toml::Value::as_bool);
    assert_eq!(notice, Some(true));
}

#[test]
fn blocking_builder_set_realtime_audio_persists_and_clears() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .set_realtime_microphone(Some("USB Mic"))
        .set_realtime_speaker(Some("Desk Speakers"))
        .apply_blocking()
        .expect("persist realtime audio");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let config: TomlValue = toml::from_str(&raw).expect("parse config");
    let realtime_audio = config
        .get("audio")
        .and_then(TomlValue::as_table)
        .expect("audio table should exist");
    assert_eq!(
        realtime_audio.get("microphone").and_then(TomlValue::as_str),
        Some("USB Mic")
    );
    assert_eq!(
        realtime_audio.get("speaker").and_then(TomlValue::as_str),
        Some("Desk Speakers")
    );

    ConfigEditsBuilder::new(codex_home)
        .set_realtime_microphone(/*microphone*/ None)
        .apply_blocking()
        .expect("clear realtime microphone");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let config: TomlValue = toml::from_str(&raw).expect("parse config");
    let realtime_audio = config
        .get("audio")
        .and_then(TomlValue::as_table)
        .expect("audio table should exist");
    assert_eq!(realtime_audio.get("microphone"), None);
    assert_eq!(
        realtime_audio.get("speaker").and_then(TomlValue::as_str),
        Some("Desk Speakers")
    );
}

#[test]
fn blocking_builder_set_realtime_voice_persists_and_clears() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();

    ConfigEditsBuilder::new(codex_home)
        .set_realtime_voice(Some("cedar"))
        .apply_blocking()
        .expect("persist realtime voice");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let config: TomlValue = toml::from_str(&raw).expect("parse config");
    let realtime = config
        .get("realtime")
        .and_then(TomlValue::as_table)
        .expect("realtime table should exist");
    assert_eq!(
        realtime.get("voice").and_then(TomlValue::as_str),
        Some("cedar")
    );

    ConfigEditsBuilder::new(codex_home)
        .set_realtime_voice(/*voice*/ None)
        .apply_blocking()
        .expect("clear realtime voice");

    let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    let config: TomlValue = toml::from_str(&raw).expect("parse config");
    let realtime = config
        .get("realtime")
        .and_then(TomlValue::as_table)
        .expect("realtime table should exist");
    assert_eq!(realtime.get("voice"), None);
}

#[test]
fn replace_mcp_servers_blocking_clears_table_when_empty() {
    let tmp = tempdir().expect("tmpdir");
    let codex_home = tmp.path();
    std::fs::write(
        codex_home.join(CONFIG_TOML_FILE),
        "[mcp_servers]\nfoo = { command = \"cmd\" }\n",
    )
    .expect("seed");

    apply_blocking(
        codex_home,
        &[ConfigEdit::ReplaceMcpServers(BTreeMap::new())],
    )
    .expect("persist");

    let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
    assert!(!contents.contains("mcp_servers"));
}
