use crate::file_ops::{DeckBackupPolicy, write_text_file_durably};
use crate::learning_dashboard_model::CalibrationRecallRule;
use crate::localization::{self, Localizer};
use crate::ready_list::{ReadyCardFilter, ReadyCardSort, ReadyCardSortDirection};
use cosimo::{LocalDeckSyncState, SyncAuthSession, SyncUuid, normalize_device_name};
use std::fs;
use std::path::{Path, PathBuf};

pub(crate) const CONFIG_PATH: &str = "cosimo.ini";
pub(crate) const LEGACY_SETTINGS_PATH: &str = "cosimo-settings.txt";
pub(crate) const LEGACY_LAST_OPEN_DECK_PATH_FILE: &str = "cosimo-last-open-deck.txt";
pub(crate) const MAX_RECENT_DECKS: usize = 8;
pub(crate) const DEFAULT_NEW_CARDS_PER_DAY: usize = 20;
pub(crate) const DEFAULT_SYNC_SERVER_URL: &str = "https://cosimo.isonomia.net/sync.cgi";
pub(crate) const DISABLED_SYNC_SERVER_URL_CODE: &str = "none";

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum ReviewDbBackupPolicy {
    #[default]
    OnSessionStart,
    Off,
}

impl ReviewDbBackupPolicy {
    pub(crate) const ALL: &'static [Self] = &[Self::OnSessionStart, Self::Off];

    fn from_code(code: &str) -> Option<Self> {
        match code {
            "on-session-start" => Some(Self::OnSessionStart),
            "off" => Some(Self::Off),
            _ => None,
        }
    }

    fn code(self) -> &'static str {
        match self {
            Self::OnSessionStart => "on-session-start",
            Self::Off => "off",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum AutomaticFullDeckBackupPolicy {
    Daily,
    #[default]
    Off,
}

impl AutomaticFullDeckBackupPolicy {
    pub(crate) const ALL: &'static [Self] = &[Self::Off, Self::Daily];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "daily" => Some(Self::Daily),
            "off" => Some(Self::Off),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Daily => "daily",
            Self::Off => "off",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum SoundNotificationPolicy {
    Always,
    WhenVisible,
    WhenInTray,
    #[default]
    Never,
}

impl SoundNotificationPolicy {
    pub(crate) const ALL: &'static [Self] = &[
        Self::Never,
        Self::WhenInTray,
        Self::WhenVisible,
        Self::Always,
    ];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "always" => Some(Self::Always),
            "when-visible" => Some(Self::WhenVisible),
            "when-in-tray" => Some(Self::WhenInTray),
            "never" => Some(Self::Never),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Always => "always",
            Self::WhenVisible => "when-visible",
            Self::WhenInTray => "when-in-tray",
            Self::Never => "never",
        }
    }

    pub(crate) fn allows_notification(self, app_visible: bool, in_tray: bool) -> bool {
        match self {
            Self::Always => true,
            Self::WhenVisible => app_visible,
            Self::WhenInTray => in_tray,
            Self::Never => false,
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum ReviewAudioPolicy {
    Automatic,
    Manual,
    #[default]
    Off,
}

impl ReviewAudioPolicy {
    pub(crate) const ALL: &'static [Self] = &[Self::Off, Self::Manual, Self::Automatic];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "automatic" => Some(Self::Automatic),
            "manual" => Some(Self::Manual),
            "off" => Some(Self::Off),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Automatic => "automatic",
            Self::Manual => "manual",
            Self::Off => "off",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum AudioGenerationFormat {
    #[default]
    Opus,
    Wav,
}

impl AudioGenerationFormat {
    pub(crate) const ALL: &'static [Self] = &[Self::Opus, Self::Wav];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "opus" => Some(Self::Opus),
            "wav" | "wave" => Some(Self::Wav),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Opus => "opus",
            Self::Wav => "wav",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum ExamPostAnswerAudioPolicy {
    #[default]
    Off,
    Question,
    Answer,
    Both,
}

impl ExamPostAnswerAudioPolicy {
    pub(crate) const ALL: &'static [Self] = &[Self::Off, Self::Question, Self::Answer, Self::Both];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "off" => Some(Self::Off),
            "question" => Some(Self::Question),
            "answer" => Some(Self::Answer),
            "both" => Some(Self::Both),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Off => "off",
            Self::Question => "question",
            Self::Answer => "answer",
            Self::Both => "both",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum QuizAudioPolicy {
    #[default]
    Off,
    Prompt,
    Choices,
    Both,
}

impl QuizAudioPolicy {
    pub(crate) const ALL: &'static [Self] = &[Self::Off, Self::Prompt, Self::Choices, Self::Both];

    pub(crate) fn from_code(code: &str) -> Option<Self> {
        match code {
            "off" => Some(Self::Off),
            "prompt" => Some(Self::Prompt),
            "choices" => Some(Self::Choices),
            "both" => Some(Self::Both),
            _ => None,
        }
    }

    pub(crate) fn code(self) -> &'static str {
        match self {
            Self::Off => "off",
            Self::Prompt => "prompt",
            Self::Choices => "choices",
            Self::Both => "both",
        }
    }

    pub(crate) fn includes_prompt(self) -> bool {
        matches!(self, Self::Prompt | Self::Both)
    }

    pub(crate) fn includes_choices(self) -> bool {
        matches!(self, Self::Choices | Self::Both)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AppSettings {
    pub(crate) language: localization::Language,
    pub(crate) minimum_cards_per_session: usize,
    pub(crate) new_cards_per_day: usize,
    pub(crate) deck_backup_policy: DeckBackupPolicy,
    pub(crate) review_db_backup_policy: ReviewDbBackupPolicy,
    pub(crate) automatic_full_deck_backup_policy: AutomaticFullDeckBackupPolicy,
    pub(crate) sound_notification_policy: SoundNotificationPolicy,
    pub(crate) review_audio_policy: ReviewAudioPolicy,
    pub(crate) review_audio_cues: bool,
    pub(crate) audio_generation_format: AudioGenerationFormat,
    pub(crate) exam_post_answer_audio_policy: ExamPostAnswerAudioPolicy,
    pub(crate) quiz_audio_policy: QuizAudioPolicy,
    pub(crate) calibration_recall_rule: CalibrationRecallRule,
    pub(crate) minimize_to_tray: bool,
}

impl Default for AppSettings {
    fn default() -> Self {
        Self {
            language: localization::Language::English,
            minimum_cards_per_session: 0,
            new_cards_per_day: DEFAULT_NEW_CARDS_PER_DAY,
            deck_backup_policy: DeckBackupPolicy::Full,
            review_db_backup_policy: ReviewDbBackupPolicy::OnSessionStart,
            automatic_full_deck_backup_policy: AutomaticFullDeckBackupPolicy::Off,
            sound_notification_policy: SoundNotificationPolicy::Never,
            review_audio_policy: ReviewAudioPolicy::Off,
            review_audio_cues: false,
            audio_generation_format: AudioGenerationFormat::Opus,
            exam_post_answer_audio_policy: ExamPostAnswerAudioPolicy::Off,
            quiz_audio_policy: QuizAudioPolicy::Off,
            calibration_recall_rule: CalibrationRecallRule::default(),
            minimize_to_tray: false,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParsedAppSettings {
    pub(crate) language: Option<localization::Language>,
    pub(crate) minimum_cards_per_session: usize,
    pub(crate) new_cards_per_day: usize,
    pub(crate) deck_backup_policy: DeckBackupPolicy,
    pub(crate) review_db_backup_policy: ReviewDbBackupPolicy,
    pub(crate) automatic_full_deck_backup_policy: AutomaticFullDeckBackupPolicy,
    pub(crate) sound_notification_policy: SoundNotificationPolicy,
    pub(crate) review_audio_policy: ReviewAudioPolicy,
    pub(crate) review_audio_cues: bool,
    pub(crate) audio_generation_format: AudioGenerationFormat,
    pub(crate) exam_post_answer_audio_policy: ExamPostAnswerAudioPolicy,
    pub(crate) quiz_audio_policy: QuizAudioPolicy,
    pub(crate) calibration_recall_rule: CalibrationRecallRule,
    pub(crate) minimize_to_tray: bool,
}

impl Default for ParsedAppSettings {
    fn default() -> Self {
        Self {
            language: None,
            minimum_cards_per_session: 0,
            new_cards_per_day: DEFAULT_NEW_CARDS_PER_DAY,
            deck_backup_policy: DeckBackupPolicy::Full,
            review_db_backup_policy: ReviewDbBackupPolicy::OnSessionStart,
            automatic_full_deck_backup_policy: AutomaticFullDeckBackupPolicy::Off,
            sound_notification_policy: SoundNotificationPolicy::Never,
            review_audio_policy: ReviewAudioPolicy::Off,
            review_audio_cues: false,
            audio_generation_format: AudioGenerationFormat::Opus,
            exam_post_answer_audio_policy: ExamPostAnswerAudioPolicy::Off,
            quiz_audio_policy: QuizAudioPolicy::Off,
            calibration_recall_rule: CalibrationRecallRule::default(),
            minimize_to_tray: false,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AppConfig {
    pub(crate) settings: ParsedAppSettings,
    pub(crate) last_open_deck_path: Option<PathBuf>,
    pub(crate) recent_deck_paths: Vec<PathBuf>,
    pub(crate) ready_card_filter: ReadyCardFilter,
    pub(crate) ready_card_sort: ReadyCardSort,
    pub(crate) ready_card_sort_direction: ReadyCardSortDirection,
    pub(crate) sync_server_url: Option<String>,
    pub(crate) sync_server_url_disabled: bool,
    pub(crate) local_device_name: Option<String>,
    pub(crate) sync_owner_identity: Option<String>,
    pub(crate) sync_bearer_token: Option<String>,
    pub(crate) deck_sync_states: Vec<LocalDeckSyncState>,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            settings: ParsedAppSettings::default(),
            last_open_deck_path: None,
            recent_deck_paths: Vec::new(),
            ready_card_filter: ReadyCardFilter::All,
            ready_card_sort: ReadyCardSort::DeckOrder,
            ready_card_sort_direction: ReadyCardSort::DeckOrder.default_direction(),
            sync_server_url: Some(DEFAULT_SYNC_SERVER_URL.to_string()),
            sync_server_url_disabled: false,
            local_device_name: None,
            sync_owner_identity: None,
            sync_bearer_token: None,
            deck_sync_states: Vec::new(),
        }
    }
}

#[derive(Debug, Default)]
struct PendingDeckSyncState {
    deck_uuid: Option<SyncUuid>,
    sync_server_url: Option<String>,
    owner_identity: Option<String>,
    last_seen_remote_revision: Option<String>,
    last_uploaded_local_bundle_hash: Option<String>,
    last_downloaded_remote_bundle_hash: Option<String>,
}

impl PendingDeckSyncState {
    fn set(&mut self, key: &str, value: &str) {
        match key {
            "deck_uuid" => {
                if let Ok(uuid) = value.parse::<SyncUuid>() {
                    self.deck_uuid = Some(uuid);
                }
            }
            "sync_server_url" => self.sync_server_url = Some(value.to_string()),
            "owner_identity" => self.owner_identity = Some(value.to_string()),
            "last_seen_remote_revision" => self.last_seen_remote_revision = Some(value.to_string()),
            "last_uploaded_local_bundle_hash" => {
                self.last_uploaded_local_bundle_hash = Some(value.to_string());
            }
            "last_downloaded_remote_bundle_hash" => {
                self.last_downloaded_remote_bundle_hash = Some(value.to_string());
            }
            _ => {}
        }
    }

    fn into_state(self) -> Option<LocalDeckSyncState> {
        let sync_server_url = self.sync_server_url?;
        if sync_server_url.starts_with("local-directory:") {
            return None;
        }
        LocalDeckSyncState::from_parts(
            self.deck_uuid?,
            &sync_server_url,
            self.owner_identity.as_deref(),
            self.last_seen_remote_revision.as_deref(),
            self.last_uploaded_local_bundle_hash.as_deref(),
            self.last_downloaded_remote_bundle_hash.as_deref(),
        )
        .ok()
    }
}

#[cfg(test)]
pub(crate) fn parse_app_settings(settings_text: &str) -> AppSettings {
    let parsed = parse_app_settings_without_default_language(settings_text);
    AppSettings {
        language: parsed.language.unwrap_or(localization::Language::English),
        minimum_cards_per_session: parsed.minimum_cards_per_session,
        new_cards_per_day: parsed.new_cards_per_day,
        deck_backup_policy: parsed.deck_backup_policy,
        review_db_backup_policy: parsed.review_db_backup_policy,
        automatic_full_deck_backup_policy: parsed.automatic_full_deck_backup_policy,
        sound_notification_policy: parsed.sound_notification_policy,
        review_audio_policy: parsed.review_audio_policy,
        review_audio_cues: parsed.review_audio_cues,
        audio_generation_format: parsed.audio_generation_format,
        exam_post_answer_audio_policy: parsed.exam_post_answer_audio_policy,
        quiz_audio_policy: parsed.quiz_audio_policy,
        calibration_recall_rule: parsed.calibration_recall_rule,
        minimize_to_tray: parsed.minimize_to_tray,
    }
}

pub(crate) fn parse_app_settings_without_default_language(
    settings_text: &str,
) -> ParsedAppSettings {
    let mut settings = ParsedAppSettings::default();

    for line in settings_text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };

        match key.trim() {
            "language" => {
                if let Some(language) = localization::Language::from_code(value.trim()) {
                    settings.language = Some(language);
                }
            }
            "minimum_cards_per_session" => {
                if let Ok(minimum_cards_per_session) = value.trim().parse::<usize>() {
                    settings.minimum_cards_per_session = minimum_cards_per_session;
                }
            }
            "new_cards_per_day" => {
                if let Ok(new_cards_per_day) = value.trim().parse::<usize>() {
                    settings.new_cards_per_day = new_cards_per_day;
                }
            }
            "deck_backup_policy" => {
                if let Some(deck_backup_policy) = DeckBackupPolicy::from_code(value.trim()) {
                    settings.deck_backup_policy = deck_backup_policy;
                }
            }
            "review_db_backup_policy" => {
                if let Some(review_db_backup_policy) = ReviewDbBackupPolicy::from_code(value.trim())
                {
                    settings.review_db_backup_policy = review_db_backup_policy;
                }
            }
            "automatic_full_deck_backup_policy" => {
                if let Some(policy) = AutomaticFullDeckBackupPolicy::from_code(value.trim()) {
                    settings.automatic_full_deck_backup_policy = policy;
                }
            }
            "sound_notification_policy" => {
                if let Some(policy) = SoundNotificationPolicy::from_code(value.trim()) {
                    settings.sound_notification_policy = policy;
                }
            }
            "review_audio_policy" => {
                if let Some(policy) = ReviewAudioPolicy::from_code(value.trim()) {
                    settings.review_audio_policy = policy;
                }
            }
            "review_audio_cues" => {
                settings.review_audio_cues = parse_bool_setting(value.trim());
            }
            "audio_generation_format" => {
                if let Some(format) = AudioGenerationFormat::from_code(value.trim()) {
                    settings.audio_generation_format = format;
                }
            }
            "exam_post_answer_audio_policy" => {
                if let Some(policy) = ExamPostAnswerAudioPolicy::from_code(value.trim()) {
                    settings.exam_post_answer_audio_policy = policy;
                }
            }
            "quiz_audio_policy" => {
                if let Some(policy) = QuizAudioPolicy::from_code(value.trim()) {
                    settings.quiz_audio_policy = policy;
                }
            }
            "calibration_recall_rule" => {
                if let Some(rule) = CalibrationRecallRule::from_code(value.trim()) {
                    settings.calibration_recall_rule = rule;
                }
            }
            "sound_notifications" => {
                settings.sound_notification_policy = legacy_sound_notification_policy(value.trim());
            }
            "minimize_to_tray" => {
                settings.minimize_to_tray = parse_bool_setting(value.trim());
            }
            _ => {}
        }
    }

    settings
}

#[cfg(test)]
pub(crate) fn format_app_settings(settings: AppSettings) -> String {
    format!(
        "language={}\nminimum_cards_per_session={}\nnew_cards_per_day={}\ndeck_backup_policy={}\nreview_db_backup_policy={}\nautomatic_full_deck_backup_policy={}\nsound_notification_policy={}\nreview_audio_policy={}\nreview_audio_cues={}\naudio_generation_format={}\nexam_post_answer_audio_policy={}\nquiz_audio_policy={}\ncalibration_recall_rule={}\nminimize_to_tray={}\n",
        settings.language.code(),
        settings.minimum_cards_per_session,
        settings.new_cards_per_day,
        settings.deck_backup_policy.code(),
        settings.review_db_backup_policy.code(),
        settings.automatic_full_deck_backup_policy.code(),
        settings.sound_notification_policy.code(),
        settings.review_audio_policy.code(),
        format_bool_setting(settings.review_audio_cues),
        settings.audio_generation_format.code(),
        settings.exam_post_answer_audio_policy.code(),
        settings.quiz_audio_policy.code(),
        settings.calibration_recall_rule.code(),
        format_bool_setting(settings.minimize_to_tray)
    )
}

pub(crate) fn parse_app_config(config_text: &str) -> AppConfig {
    let mut config = AppConfig::default();
    let mut section = "";
    let mut explicit_sort_direction = None;
    let mut pending_deck_sync_state = PendingDeckSyncState::default();

    for line in config_text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
            continue;
        }
        if let Some(section_name) = line
            .strip_prefix('[')
            .and_then(|line| line.strip_suffix(']'))
        {
            if section == "sync.deck"
                && let Some(state) = std::mem::take(&mut pending_deck_sync_state).into_state()
            {
                config.deck_sync_states.push(state);
            }
            section = section_name.trim();
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let key = key.trim();
        let value = value.trim();

        match (section, key) {
            ("settings" | "", "language") => {
                if let Some(language) = localization::Language::from_code(value) {
                    config.settings.language = Some(language);
                }
            }
            ("settings" | "", "minimum_cards_per_session") => {
                if let Ok(minimum_cards_per_session) = value.parse::<usize>() {
                    config.settings.minimum_cards_per_session = minimum_cards_per_session;
                }
            }
            ("settings" | "", "new_cards_per_day") => {
                if let Ok(new_cards_per_day) = value.parse::<usize>() {
                    config.settings.new_cards_per_day = new_cards_per_day;
                }
            }
            ("settings" | "", "deck_backup_policy") => {
                if let Some(deck_backup_policy) = DeckBackupPolicy::from_code(value) {
                    config.settings.deck_backup_policy = deck_backup_policy;
                }
            }
            ("settings" | "", "review_db_backup_policy") => {
                if let Some(review_db_backup_policy) = ReviewDbBackupPolicy::from_code(value) {
                    config.settings.review_db_backup_policy = review_db_backup_policy;
                }
            }
            ("settings" | "", "automatic_full_deck_backup_policy") => {
                if let Some(policy) = AutomaticFullDeckBackupPolicy::from_code(value) {
                    config.settings.automatic_full_deck_backup_policy = policy;
                }
            }
            ("settings" | "", "sound_notification_policy") => {
                if let Some(policy) = SoundNotificationPolicy::from_code(value) {
                    config.settings.sound_notification_policy = policy;
                }
            }
            ("settings" | "", "review_audio_policy") => {
                if let Some(policy) = ReviewAudioPolicy::from_code(value) {
                    config.settings.review_audio_policy = policy;
                }
            }
            ("settings" | "", "review_audio_cues") => {
                config.settings.review_audio_cues = parse_bool_setting(value);
            }
            ("settings" | "", "audio_generation_format") => {
                if let Some(format) = AudioGenerationFormat::from_code(value) {
                    config.settings.audio_generation_format = format;
                }
            }
            ("settings" | "", "exam_post_answer_audio_policy") => {
                if let Some(policy) = ExamPostAnswerAudioPolicy::from_code(value) {
                    config.settings.exam_post_answer_audio_policy = policy;
                }
            }
            ("settings" | "", "quiz_audio_policy") => {
                if let Some(policy) = QuizAudioPolicy::from_code(value) {
                    config.settings.quiz_audio_policy = policy;
                }
            }
            ("settings" | "", "calibration_recall_rule") => {
                if let Some(rule) = CalibrationRecallRule::from_code(value) {
                    config.settings.calibration_recall_rule = rule;
                }
            }
            ("settings" | "", "sound_notifications") => {
                config.settings.sound_notification_policy = legacy_sound_notification_policy(value);
            }
            ("settings" | "", "minimize_to_tray") => {
                config.settings.minimize_to_tray = parse_bool_setting(value);
            }
            ("state" | "", "last_open_deck") if !value.is_empty() => {
                config.last_open_deck_path = Some(PathBuf::from(value));
            }
            ("state" | "", "recent_deck") if !value.is_empty() => {
                append_recent_deck_path(&mut config.recent_deck_paths, PathBuf::from(value));
            }
            ("state" | "", "ready_card_filter") => {
                if let Some(filter) = ReadyCardFilter::from_code(value) {
                    config.ready_card_filter = filter;
                }
            }
            ("state" | "", "ready_card_sort") => {
                if let Some(sort) = ReadyCardSort::from_code(value) {
                    config.ready_card_sort = sort;
                    config.ready_card_sort_direction = sort.default_direction();
                }
            }
            ("state" | "", "ready_card_sort_direction") => {
                if let Some(direction) = ReadyCardSortDirection::from_code(value) {
                    explicit_sort_direction = Some(direction);
                }
            }
            ("sync", "sync_server_url") => {
                let (sync_server_url, sync_server_url_disabled) = parse_sync_server_url(value);
                config.sync_server_url = sync_server_url;
                config.sync_server_url_disabled = sync_server_url_disabled;
            }
            ("sync", "device_name") => {
                config.local_device_name = normalize_device_name(Some(value));
            }
            ("sync", "sync_owner_identity") => {
                config.sync_owner_identity = normalize_single_line_optional_text(value);
            }
            ("sync", "sync_bearer_token") => {
                config.sync_bearer_token = normalize_sync_bearer_token(value);
            }
            ("sync.deck", _) => {
                pending_deck_sync_state.set(key, value);
            }
            _ => {}
        }
    }

    if section == "sync.deck"
        && let Some(state) = std::mem::take(&mut pending_deck_sync_state).into_state()
    {
        config.deck_sync_states.push(state);
    }

    if let Some(direction) = explicit_sort_direction {
        config.ready_card_sort_direction = direction;
    }

    if config.recent_deck_paths.is_empty()
        && let Some(last_open_deck_path) = config.last_open_deck_path.clone()
    {
        push_recent_deck_path(&mut config.recent_deck_paths, last_open_deck_path);
    }

    if config.sync_server_url.is_none() {
        config.sync_owner_identity = None;
        config.sync_bearer_token = None;
    }

    config
}

pub(crate) fn push_recent_deck_path(recent_deck_paths: &mut Vec<PathBuf>, deck_path: PathBuf) {
    recent_deck_paths.retain(|existing| existing != &deck_path);
    recent_deck_paths.insert(0, deck_path);
    recent_deck_paths.truncate(MAX_RECENT_DECKS);
}

fn append_recent_deck_path(recent_deck_paths: &mut Vec<PathBuf>, deck_path: PathBuf) {
    if recent_deck_paths.len() >= MAX_RECENT_DECKS
        || recent_deck_paths
            .iter()
            .any(|existing| existing == &deck_path)
    {
        return;
    }
    recent_deck_paths.push(deck_path);
}

#[cfg(test)]
pub(crate) fn format_app_config(
    settings: AppSettings,
    last_open_deck_path: Option<&Path>,
) -> String {
    format_parsed_app_config(
        ParsedAppSettings {
            language: Some(settings.language),
            minimum_cards_per_session: settings.minimum_cards_per_session,
            new_cards_per_day: settings.new_cards_per_day,
            deck_backup_policy: settings.deck_backup_policy,
            review_db_backup_policy: settings.review_db_backup_policy,
            automatic_full_deck_backup_policy: settings.automatic_full_deck_backup_policy,
            sound_notification_policy: settings.sound_notification_policy,
            review_audio_policy: settings.review_audio_policy,
            review_audio_cues: settings.review_audio_cues,
            audio_generation_format: settings.audio_generation_format,
            exam_post_answer_audio_policy: settings.exam_post_answer_audio_policy,
            quiz_audio_policy: settings.quiz_audio_policy,
            calibration_recall_rule: settings.calibration_recall_rule,
            minimize_to_tray: settings.minimize_to_tray,
        },
        last_open_deck_path,
        &[],
        ReadyCardFilter::All,
        ReadyCardSort::DeckOrder,
        ReadyCardSort::DeckOrder.default_direction(),
        None,
        false,
        None,
        None,
        None,
        &[],
    )
}

#[allow(clippy::too_many_arguments)]
fn format_parsed_app_config(
    settings: ParsedAppSettings,
    last_open_deck_path: Option<&Path>,
    recent_deck_paths: &[PathBuf],
    ready_card_filter: ReadyCardFilter,
    ready_card_sort: ReadyCardSort,
    ready_card_sort_direction: ReadyCardSortDirection,
    sync_server_url: Option<&str>,
    sync_server_url_disabled: bool,
    local_device_name: Option<&str>,
    sync_owner_identity: Option<&str>,
    sync_bearer_token: Option<&str>,
    deck_sync_states: &[LocalDeckSyncState],
) -> String {
    let mut config_text = format!(
        "[settings]\nminimum_cards_per_session={}\nnew_cards_per_day={}\ndeck_backup_policy={}\nreview_db_backup_policy={}\nautomatic_full_deck_backup_policy={}\nsound_notification_policy={}\nreview_audio_policy={}\nreview_audio_cues={}\naudio_generation_format={}\nexam_post_answer_audio_policy={}\nquiz_audio_policy={}\ncalibration_recall_rule={}\nminimize_to_tray={}\n",
        settings.minimum_cards_per_session,
        settings.new_cards_per_day,
        settings.deck_backup_policy.code(),
        settings.review_db_backup_policy.code(),
        settings.automatic_full_deck_backup_policy.code(),
        settings.sound_notification_policy.code(),
        settings.review_audio_policy.code(),
        format_bool_setting(settings.review_audio_cues),
        settings.audio_generation_format.code(),
        settings.exam_post_answer_audio_policy.code(),
        settings.quiz_audio_policy.code(),
        settings.calibration_recall_rule.code(),
        format_bool_setting(settings.minimize_to_tray)
    );
    if let Some(language) = settings.language {
        config_text.insert_str(
            "[settings]\n".len(),
            &format!("language={}\n", language.code()),
        );
    }
    config_text.push_str("\n[state]\n");
    if let Some(last_open_deck_path) = last_open_deck_path {
        config_text.push_str(&format!(
            "last_open_deck={}\n",
            last_open_deck_path.display()
        ));
    }
    for recent_deck_path in recent_deck_paths.iter().take(MAX_RECENT_DECKS) {
        config_text.push_str(&format!("recent_deck={}\n", recent_deck_path.display()));
    }
    config_text.push_str(&format!("ready_card_filter={}\n", ready_card_filter.code()));
    config_text.push_str(&format!("ready_card_sort={}\n", ready_card_sort.code()));
    config_text.push_str(&format!(
        "ready_card_sort_direction={}\n",
        ready_card_sort_direction.code()
    ));
    let normalized_sync_server_url = if sync_server_url_disabled {
        None
    } else {
        normalize_sync_server_url(sync_server_url)
    };
    let sync_server_url_to_write = if sync_server_url_disabled {
        None
    } else {
        normalized_sync_server_url.or_else(|| Some(DEFAULT_SYNC_SERVER_URL.to_string()))
    };
    if sync_server_url_disabled
        || sync_server_url_to_write.is_some()
        || local_device_name.is_some()
        || sync_owner_identity.is_some()
        || sync_bearer_token.is_some()
    {
        config_text.push_str("\n[sync]\n");
        if sync_server_url_disabled {
            config_text.push_str(&format!(
                "sync_server_url={DISABLED_SYNC_SERVER_URL_CODE}\n"
            ));
        } else if let Some(sync_server_url) = sync_server_url_to_write {
            config_text.push_str(&format!("sync_server_url={sync_server_url}\n"));
        }
        if let Some(device_name) = normalize_device_name(local_device_name) {
            config_text.push_str(&format!("device_name={device_name}\n"));
        }
        if !sync_server_url_disabled
            && let Some(owner_identity) =
                sync_owner_identity.and_then(normalize_single_line_optional_text)
        {
            config_text.push_str(&format!("sync_owner_identity={owner_identity}\n"));
        }
        if !sync_server_url_disabled
            && let Some(token) = sync_bearer_token.and_then(normalize_sync_bearer_token)
        {
            config_text.push_str(&format!("sync_bearer_token={token}\n"));
        }
    }
    for state in deck_sync_states {
        config_text.push_str("\n[sync.deck]\n");
        config_text.push_str(&format!("deck_uuid={}\n", state.deck_uuid()));
        config_text.push_str(&format!("sync_server_url={}\n", state.sync_server_url()));
        if let Some(owner_identity) = state.owner_identity() {
            config_text.push_str(&format!("owner_identity={owner_identity}\n"));
        }
        if let Some(revision) = state.last_seen_remote_revision() {
            config_text.push_str(&format!("last_seen_remote_revision={revision}\n"));
        }
        if let Some(hash) = state.last_uploaded_local_bundle_hash() {
            config_text.push_str(&format!("last_uploaded_local_bundle_hash={hash}\n"));
        }
        if let Some(hash) = state.last_downloaded_remote_bundle_hash() {
            config_text.push_str(&format!("last_downloaded_remote_bundle_hash={hash}\n"));
        }
    }
    config_text
}

pub(crate) fn load_app_config(config_path: impl AsRef<Path>) -> Result<AppConfig, String> {
    let config_path = config_path.as_ref();
    match fs::read_to_string(config_path) {
        Ok(config_text) => Ok(parse_app_config(&config_text)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(AppConfig::default()),
        Err(error) => {
            Err(Localizer::default().could_not_read_path(config_path, &error.to_string()))
        }
    }
}

pub(crate) fn effective_settings_from_config(
    config: &AppConfig,
    default_language: localization::Language,
) -> AppSettings {
    effective_app_settings_from(config.settings, default_language)
}

#[cfg(test)]
pub(crate) fn load_app_settings(settings_path: impl AsRef<Path>) -> Result<AppSettings, String> {
    load_app_config(settings_path)
        .map(|config| effective_settings_from_config(&config, localization::Language::English))
}

#[cfg(test)]
pub(crate) fn persist_app_settings(
    config_path: impl AsRef<Path>,
    settings: AppSettings,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.settings = ParsedAppSettings {
        language: Some(settings.language),
        minimum_cards_per_session: settings.minimum_cards_per_session,
        new_cards_per_day: settings.new_cards_per_day,
        deck_backup_policy: settings.deck_backup_policy,
        review_db_backup_policy: settings.review_db_backup_policy,
        automatic_full_deck_backup_policy: settings.automatic_full_deck_backup_policy,
        sound_notification_policy: settings.sound_notification_policy,
        review_audio_policy: settings.review_audio_policy,
        review_audio_cues: settings.review_audio_cues,
        audio_generation_format: settings.audio_generation_format,
        exam_post_answer_audio_policy: settings.exam_post_answer_audio_policy,
        quiz_audio_policy: settings.quiz_audio_policy,
        calibration_recall_rule: settings.calibration_recall_rule,
        minimize_to_tray: settings.minimize_to_tray,
    };
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn persist_app_options(
    config_path: impl AsRef<Path>,
    settings: AppSettings,
    sync_server_url: Option<&str>,
    sync_server_url_disabled: bool,
    local_device_name: Option<&str>,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.settings = ParsedAppSettings {
        language: Some(settings.language),
        minimum_cards_per_session: settings.minimum_cards_per_session,
        new_cards_per_day: settings.new_cards_per_day,
        deck_backup_policy: settings.deck_backup_policy,
        review_db_backup_policy: settings.review_db_backup_policy,
        automatic_full_deck_backup_policy: settings.automatic_full_deck_backup_policy,
        sound_notification_policy: settings.sound_notification_policy,
        review_audio_policy: settings.review_audio_policy,
        review_audio_cues: settings.review_audio_cues,
        audio_generation_format: settings.audio_generation_format,
        exam_post_answer_audio_policy: settings.exam_post_answer_audio_policy,
        quiz_audio_policy: settings.quiz_audio_policy,
        calibration_recall_rule: settings.calibration_recall_rule,
        minimize_to_tray: settings.minimize_to_tray,
    };
    updated_config.local_device_name = normalize_device_name(local_device_name);
    updated_config.sync_server_url_disabled = sync_server_url_disabled;
    updated_config.sync_server_url = if sync_server_url_disabled {
        None
    } else {
        normalize_sync_server_url(sync_server_url)
            .or_else(|| Some(DEFAULT_SYNC_SERVER_URL.to_string()))
    };
    if updated_config.sync_server_url != config.sync_server_url
        || updated_config.sync_server_url_disabled != config.sync_server_url_disabled
    {
        updated_config.sync_owner_identity = None;
        updated_config.sync_bearer_token = None;
    }
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn load_effective_app_settings(config_path: impl AsRef<Path>) -> AppSettings {
    let config = load_app_config(config_path).unwrap_or_default();
    let mut settings = effective_settings_from_config(&config, system_default_language());

    if let Ok(value) = std::env::var("COSIMO_LANGUAGE")
        && let Some(language) = localization::Language::from_code(&value)
    {
        settings.language = language;
    }

    settings
}

pub(crate) fn migrate_legacy_portable_config(
    config_path: impl AsRef<Path>,
    legacy_settings_path: impl AsRef<Path>,
    legacy_last_open_deck_path: impl AsRef<Path>,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let legacy_settings_path = legacy_settings_path.as_ref();
    let legacy_last_open_deck_path = legacy_last_open_deck_path.as_ref();
    let legacy_settings_exists = legacy_settings_path.exists();
    let legacy_last_open_deck_exists = legacy_last_open_deck_path.exists();

    if !legacy_settings_exists && !legacy_last_open_deck_exists {
        return Ok(());
    }

    let mut config = load_app_config(config_path)?;
    if legacy_settings_exists {
        let settings_text = fs::read_to_string(legacy_settings_path).map_err(|error| {
            Localizer::default().could_not_read_path(legacy_settings_path, &error.to_string())
        })?;
        config.settings = parse_app_settings_without_default_language(&settings_text);
    }
    if legacy_last_open_deck_exists {
        config.last_open_deck_path = read_persisted_open_deck_path(legacy_last_open_deck_path);
        if let Some(deck_path) = config.last_open_deck_path.clone() {
            push_recent_deck_path(&mut config.recent_deck_paths, deck_path);
        }
    }

    write_text_file_durably(
        config_path,
        &format_parsed_app_config(
            config.settings,
            config.last_open_deck_path.as_deref(),
            &config.recent_deck_paths,
            config.ready_card_filter,
            config.ready_card_sort,
            config.ready_card_sort_direction,
            config.sync_server_url.as_deref(),
            config.sync_server_url_disabled,
            config.local_device_name.as_deref(),
            config.sync_owner_identity.as_deref(),
            config.sync_bearer_token.as_deref(),
            &config.deck_sync_states,
        ),
    )
    .map_err(|error| Localizer::default().could_not_write_path(config_path, &error.to_string()))?;

    if legacy_settings_exists {
        fs::remove_file(legacy_settings_path).map_err(|error| {
            Localizer::default().could_not_write_path(legacy_settings_path, &error.to_string())
        })?;
    }
    if legacy_last_open_deck_exists {
        fs::remove_file(legacy_last_open_deck_path).map_err(|error| {
            Localizer::default()
                .could_not_write_path(legacy_last_open_deck_path, &error.to_string())
        })?;
    }

    Ok(())
}

pub(crate) fn persist_open_deck_path(
    config_path: impl AsRef<Path>,
    deck_path: impl AsRef<Path>,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let deck_path = deck_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.last_open_deck_path = Some(deck_path.to_path_buf());
    push_recent_deck_path(
        &mut updated_config.recent_deck_paths,
        deck_path.to_path_buf(),
    );
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn persist_ready_view_state(
    config_path: impl AsRef<Path>,
    ready_card_filter: ReadyCardFilter,
    ready_card_sort: ReadyCardSort,
    ready_card_sort_direction: ReadyCardSortDirection,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.ready_card_filter = ready_card_filter;
    updated_config.ready_card_sort = ready_card_sort;
    updated_config.ready_card_sort_direction = ready_card_sort_direction;
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn persist_sync_auth_session(
    config_path: impl AsRef<Path>,
    sync_server_url: &str,
    session: &SyncAuthSession,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.sync_server_url_disabled = false;
    updated_config.sync_server_url = Some(
        normalize_sync_server_url(Some(sync_server_url))
            .ok_or_else(|| "sync server URL must be configured before saving auth".to_string())?,
    );
    updated_config.sync_owner_identity = Some(session.owner_identity().to_string());
    updated_config.sync_bearer_token = Some(session.bearer_token().to_string());
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn clear_sync_auth_session(
    config_path: impl AsRef<Path>,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config.sync_owner_identity = None;
    updated_config.sync_bearer_token = None;
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

#[cfg(test)]
pub(crate) fn persist_deck_sync_owner_identity(
    config_path: impl AsRef<Path>,
    deck_uuid: &SyncUuid,
    sync_server_url: &str,
    owner_identity: Option<&str>,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    let previous_matching_state = updated_config
        .deck_sync_states
        .iter()
        .find(|existing| {
            existing.deck_uuid() == deck_uuid
                && existing.sync_server_url() == sync_server_url
                && existing.owner_identity() == owner_identity
        })
        .cloned();

    updated_config.deck_sync_states.retain(|existing| {
        existing.deck_uuid() != deck_uuid || existing.sync_server_url() != sync_server_url
    });

    if let Some(owner_identity) = owner_identity {
        let sync_state = LocalDeckSyncState::from_parts(
            deck_uuid.clone(),
            sync_server_url,
            Some(owner_identity),
            previous_matching_state
                .as_ref()
                .and_then(LocalDeckSyncState::last_seen_remote_revision),
            previous_matching_state
                .as_ref()
                .and_then(LocalDeckSyncState::last_uploaded_local_bundle_hash),
            previous_matching_state
                .as_ref()
                .and_then(LocalDeckSyncState::last_downloaded_remote_bundle_hash),
        )
        .map_err(|error| error.to_string())?;
        updated_config.deck_sync_states.push(sync_state);
    }

    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

#[allow(dead_code)]
pub(crate) fn persist_local_deck_sync_state(
    config_path: impl AsRef<Path>,
    state: LocalDeckSyncState,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    if let Some(existing) = updated_config
        .deck_sync_states
        .iter_mut()
        .find(|existing| existing.same_remote_slot(&state))
    {
        *existing = state;
    } else {
        updated_config.deck_sync_states.push(state);
    }
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

pub(crate) fn forget_local_deck_sync_state_for_uuid(
    config_path: impl AsRef<Path>,
    deck_uuid: &SyncUuid,
    localizer: Localizer,
) -> Result<(), String> {
    let config_path = config_path.as_ref();
    let config = load_app_config(config_path)?;
    let mut updated_config = config.clone();
    updated_config
        .deck_sync_states
        .retain(|existing| existing.deck_uuid() != deck_uuid);
    write_app_config_if_changed(config_path, &config, &updated_config, localizer)
}

fn write_app_config_if_changed(
    config_path: &Path,
    current_config: &AppConfig,
    updated_config: &AppConfig,
    localizer: Localizer,
) -> Result<(), String> {
    // Runtime view state can change often, so compare parsed settings before
    // writing. An unchanged file is left byte-for-byte alone unless it still
    // needs the explicit default sync URL introduced with the public server.
    if current_config == updated_config
        && !app_config_needs_explicit_default_sync_url(config_path, updated_config, localizer)?
    {
        return Ok(());
    }

    write_text_file_durably(
        config_path,
        &format_parsed_app_config(
            updated_config.settings,
            updated_config.last_open_deck_path.as_deref(),
            &updated_config.recent_deck_paths,
            updated_config.ready_card_filter,
            updated_config.ready_card_sort,
            updated_config.ready_card_sort_direction,
            updated_config.sync_server_url.as_deref(),
            updated_config.sync_server_url_disabled,
            updated_config.local_device_name.as_deref(),
            updated_config.sync_owner_identity.as_deref(),
            updated_config.sync_bearer_token.as_deref(),
            &updated_config.deck_sync_states,
        ),
    )
    .map_err(|error| localizer.could_not_write_path(config_path, &error.to_string()))
}

fn app_config_needs_explicit_default_sync_url(
    config_path: &Path,
    updated_config: &AppConfig,
    localizer: Localizer,
) -> Result<bool, String> {
    if updated_config.sync_server_url_disabled
        || updated_config.sync_server_url.as_deref() != Some(DEFAULT_SYNC_SERVER_URL)
    {
        return Ok(false);
    }

    match fs::read_to_string(config_path) {
        Ok(config_text) => Ok(!app_config_text_has_sync_server_url(&config_text)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(true),
        Err(error) => Err(localizer.could_not_read_path(config_path, &error.to_string())),
    }
}

fn app_config_text_has_sync_server_url(config_text: &str) -> bool {
    let mut section = "";
    for line in config_text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
            continue;
        }
        if let Some(stripped) = line
            .strip_prefix('[')
            .and_then(|line| line.strip_suffix(']'))
        {
            section = stripped.trim();
            continue;
        }
        if section == "sync"
            && let Some((key, _value)) = line.split_once('=')
            && key.trim() == "sync_server_url"
        {
            return true;
        }
    }
    false
}

fn read_persisted_open_deck_path(state_path: impl AsRef<Path>) -> Option<PathBuf> {
    let state_path = state_path.as_ref();
    let saved_path = fs::read_to_string(state_path).ok()?;
    let saved_path = saved_path.trim();
    if saved_path.is_empty() {
        None
    } else {
        Some(PathBuf::from(saved_path))
    }
}

pub(crate) fn effective_app_settings_from(
    parsed_settings: ParsedAppSettings,
    default_language: localization::Language,
) -> AppSettings {
    AppSettings {
        language: parsed_settings.language.unwrap_or(default_language),
        minimum_cards_per_session: parsed_settings.minimum_cards_per_session,
        new_cards_per_day: parsed_settings.new_cards_per_day,
        deck_backup_policy: parsed_settings.deck_backup_policy,
        review_db_backup_policy: parsed_settings.review_db_backup_policy,
        automatic_full_deck_backup_policy: parsed_settings.automatic_full_deck_backup_policy,
        sound_notification_policy: parsed_settings.sound_notification_policy,
        review_audio_policy: parsed_settings.review_audio_policy,
        review_audio_cues: parsed_settings.review_audio_cues,
        audio_generation_format: parsed_settings.audio_generation_format,
        exam_post_answer_audio_policy: parsed_settings.exam_post_answer_audio_policy,
        quiz_audio_policy: parsed_settings.quiz_audio_policy,
        calibration_recall_rule: parsed_settings.calibration_recall_rule,
        minimize_to_tray: parsed_settings.minimize_to_tray,
    }
}

fn legacy_sound_notification_policy(value: &str) -> SoundNotificationPolicy {
    if parse_bool_setting(value) {
        SoundNotificationPolicy::Always
    } else {
        SoundNotificationPolicy::Never
    }
}

fn parse_bool_setting(value: &str) -> bool {
    matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "true" | "yes" | "on" | "1"
    )
}

fn format_bool_setting(value: bool) -> &'static str {
    if value { "true" } else { "false" }
}

pub(crate) fn normalize_sync_server_url(value: Option<&str>) -> Option<String> {
    let value = value?.trim();
    if sync_server_url_text_disables_sync(value) || value.contains('\n') || value.contains('\r') {
        None
    } else {
        Some(value.to_string())
    }
}

pub(crate) fn sync_server_url_text_disables_sync(value: &str) -> bool {
    let value = value.trim();
    value.is_empty()
        || value.eq_ignore_ascii_case(DISABLED_SYNC_SERVER_URL_CODE)
        || value.eq_ignore_ascii_case("off")
        || value.eq_ignore_ascii_case("disabled")
        || value.starts_with("local-directory:")
}

fn parse_sync_server_url(value: &str) -> (Option<String>, bool) {
    if sync_server_url_text_disables_sync(value) {
        (None, true)
    } else {
        (normalize_sync_server_url(Some(value)), false)
    }
}

fn normalize_single_line_optional_text(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty() || value.contains('\n') || value.contains('\r') {
        None
    } else {
        Some(value.to_string())
    }
}

fn normalize_sync_bearer_token(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty() || value.bytes().any(|byte| byte.is_ascii_whitespace()) {
        None
    } else {
        Some(value.to_string())
    }
}

fn system_default_language() -> localization::Language {
    language_for_locale_codes(system_locale_codes())
}

pub(crate) fn language_for_locale_codes(
    codes: impl IntoIterator<Item = String>,
) -> localization::Language {
    codes
        .into_iter()
        .find_map(|code| localization::Language::from_code(&code))
        .unwrap_or(localization::Language::English)
}

#[cfg(windows)]
fn system_locale_codes() -> Vec<String> {
    windows_user_locale_name().into_iter().collect()
}

#[cfg(windows)]
fn windows_user_locale_name() -> Option<String> {
    use windows_sys::Win32::Globalization::GetUserDefaultLocaleName;
    use windows_sys::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;

    let mut buffer = [0u16; LOCALE_NAME_MAX_LENGTH as usize];
    // SAFETY: buffer is a valid writable UTF-16 buffer with exactly the length
    // passed to the Windows API. The API writes at most that many elements and
    // returns the number of UTF-16 code units written, including the terminator.
    let length =
        unsafe { GetUserDefaultLocaleName(buffer.as_mut_ptr(), LOCALE_NAME_MAX_LENGTH as i32) };
    if length <= 1 {
        return None;
    }

    Some(String::from_utf16_lossy(&buffer[..length as usize - 1]))
}

#[cfg(not(windows))]
fn system_locale_codes() -> Vec<String> {
    let mut codes = Vec::new();
    for variable in ["LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"] {
        let Ok(value) = std::env::var(variable) else {
            continue;
        };

        codes.extend(
            value
                .split(':')
                .map(normalize_locale_code)
                .filter(|code| !code.is_empty()),
        );
    }
    codes
}

#[cfg(not(windows))]
fn normalize_locale_code(code: &str) -> String {
    code.split('.').next().unwrap_or(code).trim().to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn unique_temp_dir(name: &str) -> PathBuf {
        let mut path = std::env::temp_dir();
        path.push(format!("cosimo-app-config-{name}-{}", std::process::id()));
        if path.exists() {
            std::fs::remove_dir_all(&path).unwrap();
        }
        std::fs::create_dir_all(&path).unwrap();
        path
    }

    #[test]
    fn review_database_backup_policy_codes_round_trip_for_all_options() {
        for policy in ReviewDbBackupPolicy::ALL.iter().copied() {
            assert_eq!(ReviewDbBackupPolicy::from_code(policy.code()), Some(policy));
        }
    }

    #[test]
    fn automatic_full_deck_backup_policy_codes_round_trip_for_all_options() {
        for policy in AutomaticFullDeckBackupPolicy::ALL.iter().copied() {
            assert_eq!(
                AutomaticFullDeckBackupPolicy::from_code(policy.code()),
                Some(policy)
            );
        }
    }

    #[test]
    fn sound_notification_policy_codes_round_trip_for_all_options() {
        for policy in SoundNotificationPolicy::ALL.iter().copied() {
            assert_eq!(
                SoundNotificationPolicy::from_code(policy.code()),
                Some(policy)
            );
        }
    }

    #[test]
    fn review_audio_policy_codes_round_trip_for_all_options() {
        for policy in ReviewAudioPolicy::ALL.iter().copied() {
            assert_eq!(ReviewAudioPolicy::from_code(policy.code()), Some(policy));
        }
    }

    #[test]
    fn exam_post_answer_audio_policy_codes_round_trip_for_all_options() {
        for policy in ExamPostAnswerAudioPolicy::ALL.iter().copied() {
            assert_eq!(
                ExamPostAnswerAudioPolicy::from_code(policy.code()),
                Some(policy)
            );
        }
    }

    #[test]
    fn quiz_audio_policy_codes_round_trip_for_all_options() {
        for policy in QuizAudioPolicy::ALL.iter().copied() {
            assert_eq!(QuizAudioPolicy::from_code(policy.code()), Some(policy));
        }
    }

    #[test]
    fn default_config_uses_public_sync_server_url() {
        assert_eq!(
            AppConfig::default().sync_server_url.as_deref(),
            Some(DEFAULT_SYNC_SERVER_URL)
        );
        assert!(!AppConfig::default().sync_server_url_disabled);
        assert_eq!(
            parse_app_config("[settings]\nlanguage=en\n")
                .sync_server_url
                .as_deref(),
            Some(DEFAULT_SYNC_SERVER_URL)
        );
        assert!(!parse_app_config("[settings]\nlanguage=en\n").sync_server_url_disabled);
    }

    #[test]
    fn parse_app_config_reads_local_sync_settings() {
        let config = parse_app_config(
            "\
[sync]
sync_server_url=https://sync.example.invalid
device_name= laptop
sync_owner_identity= acct:modulus@example.invalid
sync_bearer_token= cosimo_token
",
        );

        assert_eq!(
            config.sync_server_url.as_deref(),
            Some("https://sync.example.invalid")
        );
        assert!(!config.sync_server_url_disabled);
        assert_eq!(config.local_device_name.as_deref(), Some("laptop"));
        assert_eq!(
            config.sync_owner_identity.as_deref(),
            Some("acct:modulus@example.invalid")
        );
        assert_eq!(config.sync_bearer_token.as_deref(), Some("cosimo_token"));
    }

    #[test]
    fn parse_app_config_ignores_invalid_local_sync_settings() {
        let config = parse_app_config(
            "\
[sync]
sync_server_url=
device_name=
sync_bearer_token=two words
",
        );

        assert_eq!(config.sync_server_url, None);
        assert!(config.sync_server_url_disabled);
        assert_eq!(config.local_device_name, None);
        assert_eq!(config.sync_owner_identity, None);
        assert_eq!(config.sync_bearer_token, None);
    }

    #[test]
    fn parse_app_config_reads_explicit_sync_disabled_code() {
        let config = parse_app_config(
            "\
[sync]
sync_server_url=None
sync_owner_identity=acct:modulus@example.invalid
sync_bearer_token=cosimo_token
",
        );

        assert_eq!(config.sync_server_url, None);
        assert!(config.sync_server_url_disabled);
        assert_eq!(config.sync_owner_identity, None);
        assert_eq!(config.sync_bearer_token, None);
    }

    #[test]
    fn parse_app_config_reads_local_deck_sync_states() {
        let config = parse_app_config(
            "\
[sync.deck]
deck_uuid=550E8400-E29B-41D4-A716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
last_uploaded_local_bundle_hash=sha256:local
last_downloaded_remote_bundle_hash=sha256:remote

[sync.deck]
deck_uuid=650e8400-e29b-41d4-a716-446655440000
sync_server_url=https://other.example.invalid
",
        );

        assert_eq!(config.deck_sync_states.len(), 2);
        let first = &config.deck_sync_states[0];
        assert_eq!(
            first.deck_uuid().as_str(),
            "550e8400-e29b-41d4-a716-446655440000"
        );
        assert_eq!(first.sync_server_url(), "https://sync.example.invalid");
        assert_eq!(first.owner_identity(), Some("acct:modulus@example.invalid"));
        assert_eq!(first.last_seen_remote_revision(), Some("42"));
        assert_eq!(
            first.last_uploaded_local_bundle_hash(),
            Some("sha256:local")
        );
        assert_eq!(
            first.last_downloaded_remote_bundle_hash(),
            Some("sha256:remote")
        );
        assert_eq!(
            config.deck_sync_states[1].deck_uuid().as_str(),
            "650e8400-e29b-41d4-a716-446655440000"
        );
    }

    #[test]
    fn parse_app_config_ignores_incomplete_local_deck_sync_states() {
        let config = parse_app_config(
            "\
[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000

[sync.deck]
sync_server_url=https://sync.example.invalid

[sync.deck]
deck_uuid=not-a-uuid
sync_server_url=https://sync.example.invalid
",
        );

        assert!(config.deck_sync_states.is_empty());
    }

    #[test]
    fn parse_app_config_ignores_legacy_local_directory_sync_states() {
        let config = parse_app_config(
            "\
[sync]
sync_server_url=local-directory:/tmp/cosimo-sync

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=local-directory:/tmp/cosimo-sync
owner_identity=acct:modulus@example.invalid
",
        );

        assert_eq!(config.sync_server_url, None);
        assert!(config.sync_server_url_disabled);
        assert!(config.deck_sync_states.is_empty());
    }

    #[test]
    fn format_parsed_app_config_writes_sync_url_when_enabled() {
        let config_text = format_parsed_app_config(
            ParsedAppSettings::default(),
            None,
            &[],
            ReadyCardFilter::All,
            ReadyCardSort::DeckOrder,
            ReadyCardSort::DeckOrder.default_direction(),
            Some(" https://sync.example.invalid "),
            false,
            Some("laptop"),
            Some("acct:modulus@example.invalid"),
            Some("cosimo_token"),
            &[],
        );

        assert!(config_text.contains("\n[sync]\n"));
        assert!(config_text.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(config_text.contains("device_name=laptop\n"));
        assert!(config_text.contains("sync_owner_identity=acct:modulus@example.invalid\n"));
        assert!(config_text.contains("sync_bearer_token=cosimo_token\n"));

        let config_without_sync = format_parsed_app_config(
            ParsedAppSettings::default(),
            None,
            &[],
            ReadyCardFilter::All,
            ReadyCardSort::DeckOrder,
            ReadyCardSort::DeckOrder.default_direction(),
            None,
            false,
            None,
            None,
            None,
            &[],
        );
        assert!(config_without_sync.contains("\n[sync]\n"));
        assert!(
            config_without_sync.contains(&format!("sync_server_url={DEFAULT_SYNC_SERVER_URL}\n"))
        );
    }

    #[test]
    fn format_parsed_app_config_writes_disabled_sync_url() {
        let config_text = format_parsed_app_config(
            ParsedAppSettings::default(),
            None,
            &[],
            ReadyCardFilter::All,
            ReadyCardSort::DeckOrder,
            ReadyCardSort::DeckOrder.default_direction(),
            None,
            true,
            Some("laptop"),
            Some("acct:modulus@example.invalid"),
            Some("cosimo_token"),
            &[],
        );

        assert!(config_text.contains("\n[sync]\n"));
        assert!(config_text.contains("sync_server_url=none\n"));
        assert!(config_text.contains("device_name=laptop\n"));
        assert!(!config_text.contains("sync_owner_identity="));
        assert!(!config_text.contains("sync_bearer_token="));
    }

    #[test]
    fn format_parsed_app_config_writes_local_deck_sync_states() {
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
        let state = LocalDeckSyncState::from_parts(
            deck_uuid,
            "https://sync.example.invalid",
            Some("acct:modulus@example.invalid"),
            Some("42"),
            Some("sha256:local"),
            Some("sha256:remote"),
        )
        .unwrap();

        let config_text = format_parsed_app_config(
            ParsedAppSettings::default(),
            None,
            &[],
            ReadyCardFilter::All,
            ReadyCardSort::DeckOrder,
            ReadyCardSort::DeckOrder.default_direction(),
            None,
            false,
            None,
            None,
            None,
            &[state],
        );

        assert!(config_text.contains("\n[sync.deck]\n"));
        assert!(config_text.contains("deck_uuid=550e8400-e29b-41d4-a716-446655440000\n"));
        assert!(config_text.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(config_text.contains("owner_identity=acct:modulus@example.invalid\n"));
        assert!(config_text.contains("last_seen_remote_revision=42\n"));
        assert!(config_text.contains("last_uploaded_local_bundle_hash=sha256:local\n"));
        assert!(config_text.contains("last_downloaded_remote_bundle_hash=sha256:remote\n"));
    }

    #[test]
    fn persist_app_settings_preserves_local_deck_sync_states() {
        let temp_dir = unique_temp_dir("persist-settings-with-deck-sync-state");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[settings]
language=es

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
",
        )
        .unwrap();
        let settings = AppSettings {
            language: localization::Language::English,
            minimum_cards_per_session: 5,
            new_cards_per_day: 20,
            ..AppSettings::default()
        };

        persist_app_settings(&config_path, settings, Localizer::default()).unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("\n[sync.deck]\n"));
        assert!(saved.contains("deck_uuid=550e8400-e29b-41d4-a716-446655440000\n"));
        assert!(saved.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(saved.contains("owner_identity=acct:modulus@example.invalid\n"));
        assert!(saved.contains("last_seen_remote_revision=42\n"));
    }

    #[test]
    fn persist_app_options_updates_local_sync_settings_without_losing_deck_sync_state() {
        let temp_dir = unique_temp_dir("persist-options-with-local-sync-settings");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[settings]
language=es

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
",
        )
        .unwrap();
        let settings = AppSettings {
            language: localization::Language::English,
            minimum_cards_per_session: 5,
            new_cards_per_day: 20,
            ..AppSettings::default()
        };

        persist_app_options(
            &config_path,
            settings,
            Some(" https://sync.example.invalid "),
            false,
            Some(" laptop "),
            Localizer::default(),
        )
        .unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("\n[sync]\n"));
        assert!(saved.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(saved.contains("device_name=laptop\n"));
        assert!(saved.contains("\n[sync.deck]\n"));
        assert!(saved.contains("deck_uuid=550e8400-e29b-41d4-a716-446655440000\n"));
        assert!(saved.contains("last_seen_remote_revision=42\n"));
    }

    #[test]
    fn persist_app_options_clears_sync_auth_when_server_changes() {
        let temp_dir = unique_temp_dir("persist-options-clears-sync-auth");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid
sync_owner_identity=acct:modulus@example.invalid
sync_bearer_token=cosimo_token
",
        )
        .unwrap();

        persist_app_options(
            &config_path,
            AppSettings::default(),
            Some("https://other.example.invalid"),
            false,
            None,
            Localizer::default(),
        )
        .unwrap();

        let config = load_app_config(&config_path).unwrap();
        assert_eq!(
            config.sync_server_url.as_deref(),
            Some("https://other.example.invalid")
        );
        assert_eq!(config.sync_owner_identity, None);
        assert_eq!(config.sync_bearer_token, None);
    }

    #[test]
    fn persist_app_options_preserves_explicit_sync_opt_out() {
        let temp_dir = unique_temp_dir("persist-options-disables-sync");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid
sync_owner_identity=acct:modulus@example.invalid
sync_bearer_token=cosimo_token
",
        )
        .unwrap();

        persist_app_options(
            &config_path,
            AppSettings::default(),
            None,
            true,
            Some(" laptop "),
            Localizer::default(),
        )
        .unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("\n[sync]\n"));
        assert!(saved.contains("sync_server_url=none\n"));
        assert!(saved.contains("device_name=laptop\n"));
        assert!(!saved.contains("sync_owner_identity="));
        assert!(!saved.contains("sync_bearer_token="));

        let config = load_app_config(&config_path).unwrap();
        assert_eq!(config.sync_server_url, None);
        assert!(config.sync_server_url_disabled);
    }

    #[test]
    fn persist_app_options_writes_default_sync_url_to_new_config() {
        let temp_dir = unique_temp_dir("persist-options-new-default-sync-url");
        let config_path = temp_dir.join("cosimo.ini");

        persist_app_options(
            &config_path,
            AppSettings::default(),
            Some(DEFAULT_SYNC_SERVER_URL),
            false,
            None,
            Localizer::default(),
        )
        .unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("\n[sync]\n"));
        assert!(saved.contains(&format!("sync_server_url={DEFAULT_SYNC_SERVER_URL}\n")));
    }

    #[test]
    fn persist_app_options_materializes_default_sync_url_in_old_config() {
        let temp_dir = unique_temp_dir("persist-options-materializes-default-sync-url");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[settings]
minimum_cards_per_session=0
",
        )
        .unwrap();

        persist_app_options(
            &config_path,
            AppSettings::default(),
            Some(DEFAULT_SYNC_SERVER_URL),
            false,
            None,
            Localizer::default(),
        )
        .unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("\n[sync]\n"));
        assert!(saved.contains(&format!("sync_server_url={DEFAULT_SYNC_SERVER_URL}\n")));
    }

    #[test]
    fn persist_sync_auth_session_writes_owner_and_token() {
        let temp_dir = unique_temp_dir("persist-sync-auth-session");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[state]
ready_card_filter=flagged
",
        )
        .unwrap();
        let session =
            SyncAuthSession::from_parts("acct:modulus@example.invalid", "cosimo_token").unwrap();

        persist_sync_auth_session(
            &config_path,
            " https://sync.example.invalid ",
            &session,
            Localizer::default(),
        )
        .unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("ready_card_filter=flagged\n"));
        assert!(saved.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(saved.contains("sync_owner_identity=acct:modulus@example.invalid\n"));
        assert!(saved.contains("sync_bearer_token=cosimo_token\n"));
    }

    #[test]
    fn clear_sync_auth_session_preserves_server_and_deck_state() {
        let temp_dir = unique_temp_dir("clear-sync-auth-session");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid
sync_owner_identity=acct:modulus@example.invalid
sync_bearer_token=cosimo_token

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
",
        )
        .unwrap();

        clear_sync_auth_session(&config_path, Localizer::default()).unwrap();

        let saved = std::fs::read_to_string(&config_path).unwrap();
        assert!(saved.contains("sync_server_url=https://sync.example.invalid\n"));
        assert!(!saved.contains("sync_owner_identity=acct:modulus@example.invalid\n"));
        assert!(!saved.contains("sync_bearer_token=cosimo_token\n"));
        assert!(saved.contains("owner_identity=acct:modulus@example.invalid\n"));
        assert!(saved.contains("last_seen_remote_revision=42\n"));
    }

    #[test]
    fn persist_deck_sync_owner_identity_preserves_matching_revision_state() {
        let temp_dir = unique_temp_dir("persist-deck-sync-owner-preserves-state");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
last_uploaded_local_bundle_hash=sha256:local
last_downloaded_remote_bundle_hash=sha256:remote
",
        )
        .unwrap();
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();

        persist_deck_sync_owner_identity(
            &config_path,
            &deck_uuid,
            "https://sync.example.invalid",
            Some("acct:modulus@example.invalid"),
            Localizer::default(),
        )
        .unwrap();

        let config = load_app_config(&config_path).unwrap();
        assert_eq!(config.deck_sync_states.len(), 1);
        let state = &config.deck_sync_states[0];
        assert_eq!(state.owner_identity(), Some("acct:modulus@example.invalid"));
        assert_eq!(state.last_seen_remote_revision(), Some("42"));
        assert_eq!(
            state.last_uploaded_local_bundle_hash(),
            Some("sha256:local")
        );
        assert_eq!(
            state.last_downloaded_remote_bundle_hash(),
            Some("sha256:remote")
        );
    }

    #[test]
    fn persist_deck_sync_owner_identity_resets_state_when_owner_changes() {
        let temp_dir = unique_temp_dir("persist-deck-sync-owner-resets-state");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42
last_uploaded_local_bundle_hash=sha256:local
last_downloaded_remote_bundle_hash=sha256:remote
",
        )
        .unwrap();
        let deck_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();

        persist_deck_sync_owner_identity(
            &config_path,
            &deck_uuid,
            "https://sync.example.invalid",
            Some("acct:other@example.invalid"),
            Localizer::default(),
        )
        .unwrap();

        let config = load_app_config(&config_path).unwrap();
        assert_eq!(config.deck_sync_states.len(), 1);
        let state = &config.deck_sync_states[0];
        assert_eq!(state.owner_identity(), Some("acct:other@example.invalid"));
        assert_eq!(state.last_seen_remote_revision(), None);
        assert_eq!(state.last_uploaded_local_bundle_hash(), None);
        assert_eq!(state.last_downloaded_remote_bundle_hash(), None);
    }

    #[test]
    fn forget_local_deck_sync_state_for_uuid_preserves_other_sync_state() {
        let temp_dir = unique_temp_dir("forget-deck-sync-state");
        let config_path = temp_dir.join("cosimo.ini");
        std::fs::write(
            &config_path,
            "\
[sync]
sync_server_url=https://sync.example.invalid
device_name=laptop
sync_owner_identity=acct:modulus@example.invalid
sync_bearer_token=cosimo_token

[sync.deck]
deck_uuid=550e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=42

[sync.deck]
deck_uuid=650e8400-e29b-41d4-a716-446655440000
sync_server_url=https://sync.example.invalid
owner_identity=acct:modulus@example.invalid
last_seen_remote_revision=7
",
        )
        .unwrap();
        let forgotten_uuid: SyncUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();

        forget_local_deck_sync_state_for_uuid(&config_path, &forgotten_uuid, Localizer::default())
            .unwrap();

        let config = load_app_config(&config_path).unwrap();
        assert_eq!(
            config.sync_server_url.as_deref(),
            Some("https://sync.example.invalid")
        );
        assert_eq!(
            config.sync_owner_identity.as_deref(),
            Some("acct:modulus@example.invalid")
        );
        assert_eq!(config.sync_bearer_token.as_deref(), Some("cosimo_token"));
        assert_eq!(config.deck_sync_states.len(), 1);
        assert_eq!(
            config.deck_sync_states[0].deck_uuid().as_str(),
            "650e8400-e29b-41d4-a716-446655440000"
        );
        assert_eq!(
            config.deck_sync_states[0].last_seen_remote_revision(),
            Some("7")
        );
    }
}
