//! Sync Mode configuration for delta vs state-based synchronization (ADR-019 Amendment)
//!
//! This module addresses Issue #346: delta-based sync doesn't scale when many documents
//! change frequently. `SyncMode` allows per-collection configuration of how documents
//! sync between peers.
//!
//! # Key Insight
//!
//! ```text
//! Current Behavior (Issue #346):
//! ├─ Cell Leader offline for 5 minutes
//! ├─ 7 soldiers sending beacons every second = 2,100 beacon updates
//! ├─ On reconnection: ALL 2,100 deltas must sync
//! └─ Documents never reach Cohort Leader (broadcast channel lags)
//!
//! With LatestOnly Mode:
//! ├─ Cell Leader offline for 5 minutes
//! ├─ Beacons configured as "LatestOnly" sync mode
//! ├─ On reconnection: Only 7 current positions sync (one per soldier)
//! └─ Sync completes in milliseconds
//! ```
//!
//! # Example
//!
//! ```
//! use peat_mesh::qos::SyncMode;
//!
//! // Position data - only current state matters
//! assert_eq!(SyncMode::default_for_collection("beacons"), SyncMode::LatestOnly);
//!
//! // Audit data - full history required
//! assert_eq!(SyncMode::default_for_collection("commands"), SyncMode::FullHistory);
//! ```

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::{Mutex, OnceLock, RwLock};

/// Determines how much document history syncs between peers (ADR-019 Amendment)
///
/// This is critical for Issue #346: delta-based sync doesn't scale when many documents
/// change frequently. `LatestOnly` mode syncs only current state, reducing reconnection
/// traffic by up to 300× for high-frequency data like beacons.
///
/// # Implementation
///
/// - **FullHistory**: Uses `generate_sync_message()` - delta-based sync protocol
/// - **LatestOnly**: Uses `doc.save()` - sends full document state, no history
/// - **WindowedHistory**: Uses time-filtered `generate_sync_message()` (Phase 2)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum SyncMode {
    /// Sync all deltas - observers see every historical change
    ///
    /// Uses Automerge's `generate_sync_message()` protocol.
    /// Best for: Audit logs, commands, contact reports.
    #[default]
    FullHistory,

    /// Sync only current document state, discard intermediate deltas
    ///
    /// Uses `doc.save()` to send full document state.
    /// Best for: Positions, status updates, health telemetry.
    /// **Reduces reconnection sync by ~300× for high-frequency data.**
    LatestOnly,

    /// Sync deltas within a time window, discard older (Phase 2)
    ///
    /// Uses filtered `generate_sync_message()`.
    /// Best for: Recent track history, last N minutes of updates.
    WindowedHistory {
        /// Time window in seconds - only sync changes within this window
        window_seconds: u64,
    },
}

impl SyncMode {
    /// Returns true if this mode syncs full delta history
    #[inline]
    pub fn is_full_history(&self) -> bool {
        matches!(self, Self::FullHistory)
    }

    /// Returns true if this mode syncs only current state
    #[inline]
    pub fn is_latest_only(&self) -> bool {
        matches!(self, Self::LatestOnly)
    }

    /// Returns true if this mode uses windowed history
    #[inline]
    pub fn is_windowed(&self) -> bool {
        matches!(self, Self::WindowedHistory { .. })
    }

    /// Get the window size in seconds, if applicable
    pub fn window_seconds(&self) -> Option<u64> {
        match self {
            Self::WindowedHistory { window_seconds } => Some(*window_seconds),
            _ => None,
        }
    }

    /// Default sync mode for a collection name
    ///
    /// Returns appropriate defaults based on collection semantics:
    /// - beacons, platforms, tracks → LatestOnly (position data)
    /// - node-states, cell-summaries, cohort-summaries, federation-summaries,
    ///   coalition-summaries, capabilities → LatestOnly (hierarchical aggregation)
    /// - contact-reports, commands, audit-logs → FullHistory (critical data)
    /// - track-history, capability-history → WindowedHistory(300) (5 minutes)
    ///
    /// Collection names use the hyphenated convention established by peat-schema
    /// (e.g. `"node-states"`, `"contact-reports"`). Underscore variants are
    /// recognised with a once-per-name deprecation warning for operator migration.
    ///
    /// # Transitional backward-compatibility
    ///
    /// Two generations of deprecated names are handled:
    ///
    /// **Underscore names (peat-mesh ≤ rc.30 convention):** `node_states`,
    /// `cell_summaries`, `cohort_summaries`, `federation_summaries`,
    /// `coalition_summaries`, `contact_reports`, `audit_logs`, `track_history`,
    /// `capability_history`. These return the correct mode and warn once;
    /// remove the transitional arm after operator migration.
    ///
    /// **Pre-ADR-066 military names (peat-mesh ≤ rc.30):** `squad_summaries`,
    /// `platoon_summaries`, `company_summaries`. Same treatment.
    pub fn default_for_collection(collection: &str) -> Self {
        match collection {
            // Position/state data - only current matters
            "beacons" | "platforms" | "tracks" | "nodes" | "cells" => Self::LatestOnly,

            // Capabilities (peat-schema canonical collection name)
            "capabilities" => Self::LatestOnly,

            // Hierarchical aggregation data — canonical hyphenated names (peat-schema convention).
            // These collections update frequently; history accumulates unbounded without LatestOnly.
            "node-states"
            | "cell-summaries"
            | "cohort-summaries"
            | "federation-summaries"
            | "coalition-summaries" => Self::LatestOnly,

            // Critical data - full history required
            "contact-reports" | "commands" | "audit-logs" | "alerts" => Self::FullHistory,

            // Track/capability history - recent window useful
            "track-history" | "capability-history" => Self::WindowedHistory {
                window_seconds: 300,
            },

            // Transitional: pre-hyphen underscore collection names (peat-mesh ≤ rc.30 convention).
            // Emit a deprecation warning and return the correct mode so upgrading operators don't
            // silently get FullHistory on persisted stores keyed under old names.
            // Remove this arm after operator migration to the hyphenated form completes.
            "node_states"
            | "cell_summaries"
            | "cohort_summaries"
            | "federation_summaries"
            | "coalition_summaries" => {
                let rename_to = match collection {
                    "node_states" => "node-states",
                    "cell_summaries" => "cell-summaries",
                    "cohort_summaries" => "cohort-summaries",
                    "federation_summaries" => "federation-summaries",
                    _ => "coalition-summaries",
                };
                let _ = warn_deprecated_collection_once(collection, rename_to);
                Self::LatestOnly
            }
            "contact_reports" => {
                let _ = warn_deprecated_collection_once(collection, "contact-reports");
                Self::FullHistory
            }
            "audit_logs" => {
                let _ = warn_deprecated_collection_once(collection, "audit-logs");
                Self::FullHistory
            }
            "track_history" => {
                let _ = warn_deprecated_collection_once(collection, "track-history");
                Self::WindowedHistory {
                    window_seconds: 300,
                }
            }
            "capability_history" => {
                let _ = warn_deprecated_collection_once(collection, "capability-history");
                Self::WindowedHistory {
                    window_seconds: 300,
                }
            }

            // Transitional: pre-ADR-066 military hierarchy names. Same treatment.
            "squad_summaries" | "platoon_summaries" | "company_summaries" => {
                let rename_to = match collection {
                    "squad_summaries" => "cell-summaries",
                    "platoon_summaries" => "cohort-summaries",
                    _ => "federation-summaries",
                };
                let _ = warn_deprecated_collection_once(collection, rename_to);
                Self::LatestOnly
            }

            // Default to FullHistory for unknown collections
            _ => Self::FullHistory,
        }
    }
}

impl fmt::Display for SyncMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::FullHistory => write!(f, "FullHistory"),
            Self::LatestOnly => write!(f, "LatestOnly"),
            Self::WindowedHistory { window_seconds } => {
                write!(f, "WindowedHistory({}s)", window_seconds)
            }
        }
    }
}

/// Process-wide once-seen set for transitional collection-name deprecation warnings.
///
/// `SyncMode::default_for_collection` is called from `SyncModeRegistry::get`
/// on every miss against the overrides cache, including the hot path for
/// any collection not pre-populated in `SyncModeRegistry::with_defaults`.
/// Without this guard, an operator with a single persisted deprecated
/// collection would get a `tracing::warn!` line on every sync tick.
/// The guard collapses that to one log line per distinct old name for the
/// lifetime of the process.
///
/// Returns `true` if the warning was newly emitted (first sight of this
/// name), `false` if it was suppressed (already seen). The return value
/// is unused in the production code path; it exists so the
/// `warn_deprecated_collection_emits_once_per_distinct_name` regression
/// test can pin the once-emission contract without needing a tracing
/// subscriber.
///
/// `contains()` probes the set before constructing the owned `String` —
/// post-first-call the hot path stays allocation-free.
///
/// Removed alongside the transitional arms in a future release once
/// operator migration completes.
fn warn_deprecated_collection_once(collection: &str, rename_to: &str) -> bool {
    static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
    let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
    let mut seen = seen.lock().unwrap_or_else(|e| e.into_inner());
    if seen.contains(collection) {
        return false;
    }
    seen.insert(collection.to_string());
    tracing::warn!(
        collection = collection,
        rename_to = rename_to,
        "deprecated collection name; rename to `{rename_to}`. \
         Returning correct sync mode for now; this transitional arm \
         will be removed in a future release."
    );
    true
}

/// Registry for per-collection sync mode configuration
///
/// Allows runtime configuration of sync modes per collection,
/// with sensible defaults based on collection names.
///
/// # Example
///
/// ```
/// use peat_mesh::qos::{SyncMode, SyncModeRegistry};
///
/// let registry = SyncModeRegistry::with_defaults();
///
/// // Beacons default to LatestOnly
/// assert_eq!(registry.get("beacons"), SyncMode::LatestOnly);
///
/// // Override for a specific collection
/// registry.set("custom_collection", SyncMode::LatestOnly);
/// ```
#[derive(Debug, Default)]
pub struct SyncModeRegistry {
    /// Per-collection sync mode overrides
    overrides: RwLock<HashMap<String, SyncMode>>,
}

impl SyncModeRegistry {
    /// Create a new empty registry (uses defaults for all collections)
    pub fn new() -> Self {
        Self {
            overrides: RwLock::new(HashMap::new()),
        }
    }

    /// Create a registry with standard defaults pre-configured
    pub fn with_defaults() -> Self {
        let registry = Self::new();
        // Pre-populate with known collections for better performance.
        // Names use the canonical hyphenated peat-schema convention.
        //
        // Legacy underscored keys (from stores written by peat-mesh ≤ rc.30)
        // are intentionally NOT pre-populated: they fall through to
        // `default_for_collection`, which returns the correct mode AND emits the
        // once-gated deprecation warning. Pre-populating them would suppress the
        // migration signal, so the override fast path is reserved for canonical
        // names only. The once-gate bounds the fall-through cost to one warn per
        // distinct legacy name per process.
        let defaults = [
            ("beacons", SyncMode::LatestOnly),
            ("platforms", SyncMode::LatestOnly),
            ("tracks", SyncMode::LatestOnly),
            ("nodes", SyncMode::LatestOnly),
            ("cells", SyncMode::LatestOnly),
            ("capabilities", SyncMode::LatestOnly),
            // Hierarchical aggregation collections (canonical hyphenated names)
            ("node-states", SyncMode::LatestOnly),
            ("cell-summaries", SyncMode::LatestOnly),
            ("cohort-summaries", SyncMode::LatestOnly),
            ("federation-summaries", SyncMode::LatestOnly),
            ("coalition-summaries", SyncMode::LatestOnly),
            // Critical data - full history required
            ("contact-reports", SyncMode::FullHistory),
            ("commands", SyncMode::FullHistory),
            ("audit-logs", SyncMode::FullHistory),
            ("alerts", SyncMode::FullHistory),
            (
                "track-history",
                SyncMode::WindowedHistory {
                    window_seconds: 300,
                },
            ),
            (
                "capability-history",
                SyncMode::WindowedHistory {
                    window_seconds: 300,
                },
            ),
        ];

        {
            let mut overrides = registry
                .overrides
                .write()
                .unwrap_or_else(|e| e.into_inner());
            for (collection, mode) in defaults {
                overrides.insert(collection.to_string(), mode);
            }
        }

        registry
    }

    /// Get the sync mode for a collection
    ///
    /// Returns the configured override if set, otherwise the default for that collection.
    pub fn get(&self, collection: &str) -> SyncMode {
        self.overrides
            .read()
            .unwrap_or_else(|e| e.into_inner())
            .get(collection)
            .copied()
            .unwrap_or_else(|| SyncMode::default_for_collection(collection))
    }

    /// Set the sync mode for a collection
    pub fn set(&self, collection: &str, mode: SyncMode) {
        self.overrides
            .write()
            .unwrap_or_else(|e| e.into_inner())
            .insert(collection.to_string(), mode);
    }

    /// Remove a collection override (will use default)
    pub fn remove(&self, collection: &str) -> Option<SyncMode> {
        self.overrides
            .write()
            .unwrap_or_else(|e| e.into_inner())
            .remove(collection)
    }

    /// Get all configured overrides
    pub fn all_overrides(&self) -> HashMap<String, SyncMode> {
        self.overrides
            .read()
            .unwrap_or_else(|e| e.into_inner())
            .clone()
    }

    /// Check if a collection is configured for LatestOnly mode
    ///
    /// Convenience method for the sync coordinator.
    #[inline]
    pub fn is_latest_only(&self, collection: &str) -> bool {
        self.get(collection).is_latest_only()
    }

    /// Check if a collection is configured for FullHistory mode
    #[inline]
    pub fn is_full_history(&self, collection: &str) -> bool {
        self.get(collection).is_full_history()
    }
}

impl Clone for SyncModeRegistry {
    fn clone(&self) -> Self {
        Self {
            overrides: RwLock::new(
                self.overrides
                    .read()
                    .unwrap_or_else(|e| e.into_inner())
                    .clone(),
            ),
        }
    }
}

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

    #[test]
    fn test_sync_mode_defaults() {
        // Position data should be LatestOnly
        assert_eq!(
            SyncMode::default_for_collection("beacons"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("platforms"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("tracks"),
            SyncMode::LatestOnly
        );

        // Hierarchical aggregation should be LatestOnly (Issue #401 - memory blowout fix)
        // Canonical hyphenated names (peat-schema convention)
        assert_eq!(
            SyncMode::default_for_collection("node-states"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("cell-summaries"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("cohort-summaries"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("federation-summaries"),
            SyncMode::LatestOnly
        );
        assert_eq!(
            SyncMode::default_for_collection("coalition-summaries"),
            SyncMode::LatestOnly
        );

        // peat-schema canonical capability collection
        assert_eq!(
            SyncMode::default_for_collection("capabilities"),
            SyncMode::LatestOnly
        );

        // Critical data should be FullHistory
        assert_eq!(
            SyncMode::default_for_collection("commands"),
            SyncMode::FullHistory
        );
        assert_eq!(
            SyncMode::default_for_collection("contact-reports"),
            SyncMode::FullHistory
        );
        assert_eq!(
            SyncMode::default_for_collection("audit-logs"),
            SyncMode::FullHistory
        );

        // Track history should be windowed
        assert_eq!(
            SyncMode::default_for_collection("track-history"),
            SyncMode::WindowedHistory {
                window_seconds: 300
            }
        );
        assert_eq!(
            SyncMode::default_for_collection("capability-history"),
            SyncMode::WindowedHistory {
                window_seconds: 300
            }
        );

        // Unknown should default to FullHistory
        assert_eq!(
            SyncMode::default_for_collection("unknown"),
            SyncMode::FullHistory
        );
    }

    /// Pin backward-compat for pre-hyphen underscore collection names.
    ///
    /// peat-mesh ≤ rc.30 used underscore-separated names (`node_states`,
    /// `cell_summaries`, etc.) for all multi-word collections. rc.31 adopts
    /// the canonical hyphenated form matching peat-schema's type_registry
    /// convention (`node-states`, `cell-summaries`, etc.). The transitional
    /// arms in `default_for_collection` ensure operators with persisted stores
    /// keyed under old names still get the correct sync mode rather than
    /// silently falling through to `FullHistory`.
    #[test]
    fn pre_hyphen_collection_names_still_return_correct_mode() {
        for old_name in [
            "node_states",
            "cell_summaries",
            "cohort_summaries",
            "federation_summaries",
            "coalition_summaries",
        ] {
            assert_eq!(
                SyncMode::default_for_collection(old_name),
                SyncMode::LatestOnly,
                "transitional arm must return LatestOnly for pre-hyphen name {old_name:?}"
            );
        }
        assert_eq!(
            SyncMode::default_for_collection("contact_reports"),
            SyncMode::FullHistory,
            "transitional arm must return FullHistory for pre-hyphen contact_reports"
        );
        assert_eq!(
            SyncMode::default_for_collection("audit_logs"),
            SyncMode::FullHistory,
            "transitional arm must return FullHistory for pre-hyphen audit_logs"
        );
        assert_eq!(
            SyncMode::default_for_collection("track_history"),
            SyncMode::WindowedHistory {
                window_seconds: 300
            },
            "transitional arm must return WindowedHistory for pre-hyphen track_history"
        );
        assert_eq!(
            SyncMode::default_for_collection("capability_history"),
            SyncMode::WindowedHistory {
                window_seconds: 300
            },
            "transitional arm must return WindowedHistory for pre-hyphen capability_history"
        );
    }

    /// Pin the transitional pre-ADR-066 collection-name recognition.
    ///
    /// peat-mesh ≤ rc.30 used `squad_summaries` / `platoon_summaries` /
    /// `company_summaries` for hierarchical aggregation; rc.31 renames
    /// them to `cell-summaries` / `cohort-summaries` / `federation-summaries`
    /// per ADR-066. Operators upgrading with persisted Automerge stores
    /// keyed on the old names would silently fall through to `FullHistory`
    /// (unbounded) and only notice via disk-growth — peat-mesh#208 QA
    /// surfaced this as a [WARNING].
    ///
    /// The transitional arm in `default_for_collection` recognises the
    /// old names and returns the documented `LatestOnly`. This test pins
    /// that property so a future "clean up the transitional arm" commit
    /// has to explicitly delete the recognition + this test together.
    #[test]
    fn pre_adr066_collection_names_still_return_latest_only() {
        for old_name in ["squad_summaries", "platoon_summaries", "company_summaries"] {
            assert_eq!(
                SyncMode::default_for_collection(old_name),
                SyncMode::LatestOnly,
                "transitional arm must return LatestOnly for pre-ADR-066 name {old_name:?} \
                 to prevent silent unbounded-history regression on upgrade from rc.30 \
                 (peat-mesh#208 QA WARNING)"
            );
        }
    }

    /// Pin the once-per-distinct-name suppression contract of the
    /// transitional deprecation warning.
    ///
    /// `warn_deprecated_collection_once` returns `true` when it actually
    /// emits the `tracing::warn!` line and `false` when the gate
    /// suppresses it. The production code path discards the return
    /// value; this test uses it to assert that:
    ///
    /// 1. The first sight of a given old-name fires (`true`).
    /// 2. Any subsequent sight of the same name is suppressed (`false`).
    /// 3. A different old-name re-fires once on its own first sight,
    ///    regardless of which other names have already been emitted.
    ///
    /// Without this pin, a future refactor could silently drop the
    /// `OnceLock<Mutex<HashSet>>` gate (or invert the contains-probe)
    /// and leave operators eating one log line per sync tick — observable
    /// only by reading deployed logs. peat-mesh#208 QA SUGGESTION.
    ///
    /// `pre_adr066_collection_names_still_return_latest_only` above
    /// already exercises the gate as a side effect (each match-arm hit
    /// goes through the once-gate), so this test uses a name that
    /// wasn't seen by that test to start from a clean process-wide
    /// `SEEN` set. NOTE: the `SEEN` set is process-wide, so other tests
    /// running in this test binary that also probe the gate would
    /// confound the first-call assertion. Today only the LatestOnly
    /// pin test exercises the gate, and it does so for the three
    /// match-arm names — this test uses a *fourth* name passed through
    /// the helper directly to avoid coupling to test execution order.
    #[test]
    fn warn_deprecated_collection_emits_once_per_distinct_name() {
        // Fresh names — not in the match arm, not exercised by any other
        // test — so process-wide SEEN can't have seen them.
        let alpha = "peat-mesh-208-test-alpha";
        let beta = "peat-mesh-208-test-beta";

        assert!(
            warn_deprecated_collection_once(alpha, "peat-mesh-208-test-alpha-renamed"),
            "first call for {alpha:?} must emit (returns true)"
        );
        assert!(
            !warn_deprecated_collection_once(alpha, "peat-mesh-208-test-alpha-renamed"),
            "second call for {alpha:?} must be suppressed (returns false) — \
             the OnceLock<HashSet> gate at warn_deprecated_collection_once is the \
             whole point of this commit. If false: the gate has been bypassed."
        );
        assert!(
            !warn_deprecated_collection_once(alpha, "peat-mesh-208-test-alpha-renamed"),
            "third call still suppressed"
        );

        assert!(
            warn_deprecated_collection_once(beta, "peat-mesh-208-test-beta-renamed"),
            "distinct name {beta:?} must emit on its own first sight, \
             regardless of {alpha:?} having already fired"
        );
        assert!(
            !warn_deprecated_collection_once(beta, "peat-mesh-208-test-beta-renamed"),
            "second call for {beta:?} must be suppressed"
        );
    }

    #[test]
    fn test_sync_mode_predicates() {
        assert!(SyncMode::FullHistory.is_full_history());
        assert!(!SyncMode::FullHistory.is_latest_only());
        assert!(!SyncMode::FullHistory.is_windowed());

        assert!(SyncMode::LatestOnly.is_latest_only());
        assert!(!SyncMode::LatestOnly.is_full_history());
        assert!(!SyncMode::LatestOnly.is_windowed());

        let windowed = SyncMode::WindowedHistory { window_seconds: 60 };
        assert!(windowed.is_windowed());
        assert!(!windowed.is_full_history());
        assert!(!windowed.is_latest_only());
        assert_eq!(windowed.window_seconds(), Some(60));
    }

    #[test]
    fn test_sync_mode_display() {
        assert_eq!(SyncMode::FullHistory.to_string(), "FullHistory");
        assert_eq!(SyncMode::LatestOnly.to_string(), "LatestOnly");
        assert_eq!(
            SyncMode::WindowedHistory {
                window_seconds: 300
            }
            .to_string(),
            "WindowedHistory(300s)"
        );
    }

    #[test]
    fn test_sync_mode_registry() {
        let registry = SyncModeRegistry::with_defaults();

        // Check defaults
        assert_eq!(registry.get("beacons"), SyncMode::LatestOnly);
        assert_eq!(registry.get("commands"), SyncMode::FullHistory);

        // Override
        registry.set("beacons", SyncMode::FullHistory);
        assert_eq!(registry.get("beacons"), SyncMode::FullHistory);

        // Remove override
        registry.remove("beacons");
        assert_eq!(registry.get("beacons"), SyncMode::LatestOnly);
    }

    #[test]
    fn test_sync_mode_registry_convenience_methods() {
        let registry = SyncModeRegistry::with_defaults();

        assert!(registry.is_latest_only("beacons"));
        assert!(!registry.is_latest_only("commands"));

        assert!(registry.is_full_history("commands"));
        assert!(!registry.is_full_history("beacons"));
    }

    #[test]
    fn test_sync_mode_serialization() {
        let mode = SyncMode::LatestOnly;
        let json = serde_json::to_string(&mode).unwrap();
        assert_eq!(json, "\"LatestOnly\"");

        let deserialized: SyncMode = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, SyncMode::LatestOnly);

        // Test windowed
        let windowed = SyncMode::WindowedHistory {
            window_seconds: 300,
        };
        let json = serde_json::to_string(&windowed).unwrap();
        let deserialized: SyncMode = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, windowed);
    }

    #[test]
    fn test_sync_mode_default() {
        // Default should be FullHistory for backwards compatibility
        assert_eq!(SyncMode::default(), SyncMode::FullHistory);
    }
}
