//! Three-Tree Sync Algorithm
//!
//! Core comparison logic for the Dropbox Nucleus-inspired sync engine.
//! Compares Local, Remote, and Synced trees to produce a SyncPlan.

use crate::drive::{FileId, FileMetadata, FileTree};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use thiserror::Error;

/// Progress callback for tracking upload/download/encrypt/decrypt progress
/// Arguments: (bytes_completed, total_bytes, file_path)
/// Uses Arc for sharing across async boundaries
pub type SyncProgressFn = Arc<dyn Fn(u64, u64, Option<&str>) + Send + Sync>;

/// Progress callback for directory scanning
/// Arguments: (files_scanned, current_path)
pub type ScanProgressFn = Arc<dyn Fn(u64, Option<&str>) + Send + Sync>;

/// Progress callback for fetching remote state
/// Arguments: (files_fetched, total_estimated)
pub type FetchProgressFn = Arc<dyn Fn(u64, u64) + Send + Sync>;

/// Progress handlers for sync operations
#[derive(Default)]
pub struct SyncProgress {
    /// Called during file uploads: (bytes_sent, total_bytes, file_path)
    pub on_upload_progress: Option<SyncProgressFn>,
    /// Called during file downloads: (bytes_received, total_bytes, file_path)
    pub on_download_progress: Option<SyncProgressFn>,
    /// Called during file encryption: (bytes_encrypted, total_bytes, file_path)
    pub on_encrypt_progress: Option<SyncProgressFn>,
    /// Called during file decryption: (bytes_decrypted, total_bytes, file_path)
    pub on_decrypt_progress: Option<SyncProgressFn>,
    /// Called during directory scanning: (files_scanned, current_path)
    pub on_scan_progress: Option<ScanProgressFn>,
    /// Called while fetching remote state: (files_fetched, total_estimated)
    pub on_fetch_state_progress: Option<FetchProgressFn>,
}

/// Result of a completed sync operation
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct SyncOutcome {
    pub files_uploaded: usize,
    pub files_downloaded: usize,
    pub files_deleted_locally: usize,
    pub files_deleted_remotely: usize,
    pub conflicts_resolved: usize,
    pub conflicts_skipped: usize,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl SyncOutcome {
    #[getter]
    fn files_uploaded(&self) -> usize {
        self.files_uploaded
    }

    #[getter]
    fn files_downloaded(&self) -> usize {
        self.files_downloaded
    }

    #[getter]
    fn files_deleted_locally(&self) -> usize {
        self.files_deleted_locally
    }

    #[getter]
    fn files_deleted_remotely(&self) -> usize {
        self.files_deleted_remotely
    }

    #[getter]
    fn conflicts_resolved(&self) -> usize {
        self.conflicts_resolved
    }

    #[getter]
    fn conflicts_skipped(&self) -> usize {
        self.conflicts_skipped
    }
}

/// Errors that can occur during sync operations
#[derive(Debug, Error)]
pub enum SyncError {
    /// Conflicts detected in non-interactive mode
    #[error("Conflicts detected: {0} file(s) have conflicts that require resolution")]
    ConflictsDetected(usize),

    /// Server rejected upload due to stale base revision
    #[error("Stale base revision for file. Server has newer version.")]
    StaleBase {
        file_id: FileId,
        current_revision_id: [u8; 32],
    },

    /// Conflict detected during upload
    #[error("Conflict for file: {message}")]
    Conflict {
        file_id: FileId,
        current_revision_id: [u8; 32],
        message: String,
    },

    /// Hash mismatch after download (integrity check failed)
    #[error("Hash mismatch for downloaded file - integrity check failed")]
    HashMismatch(FileId),

    /// Failed to load sync state from disk
    #[error("Failed to load sync state: {0}")]
    StateLoad(String),

    /// Failed to save sync state to disk
    #[error("Failed to save sync state: {0}")]
    StateSave(String),

    /// Failed to fetch remote state from server
    #[error("Failed to fetch remote state: {0}")]
    RemoteFetch(String),

    /// Upload failed
    #[error("Upload failed: {0}")]
    UploadFailed(String),

    /// Download failed
    #[error("Download failed: {0}")]
    DownloadFailed(String),

    /// Remote delete failed
    #[error("Remote delete failed: {0}")]
    RemoteDelete(String),

    /// File not found
    #[error("File not found: {0}")]
    FileNotFound(String),

    /// Network error
    #[error("Network error: {0}")]
    Network(String),

    /// Encryption error
    #[error("Encryption error: {0}")]
    Encryption(String),

    /// Decryption error
    #[error("Decryption error: {0}")]
    Decryption(String),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// JSON serialization error
    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    /// Not initialized - .hippius directory doesn't exist
    #[error("Drive not initialized. Run 'init' first.")]
    NotInitialized,

    /// User cancelled operation
    #[error("Operation cancelled by user")]
    Cancelled,

    /// Server not configured
    #[error("Server not configured. Call set_server() first.")]
    ServerNotConfigured,
}

/// Result type for sync operations
pub type SyncResult<T> = Result<T, SyncError>;

// =============================================================================
// Sync Action Classification
// =============================================================================

/// Classification of a file's sync status based on three-tree comparison
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SyncAction {
    /// File unchanged in all trees - no action needed
    Unchanged,
    /// File created locally (in local, not in synced) - upload
    LocalCreate,
    /// File modified locally (in local, differs from synced) - upload
    LocalModify,
    /// File deleted locally (in synced, not in local) - delete on server
    LocalDelete,
    /// File created remotely (in remote, not in synced) - download
    RemoteCreate,
    /// File modified remotely (in remote, differs from synced) - download
    RemoteModify,
    /// File deleted remotely (in synced, not in remote) - delete locally
    RemoteDelete,
    /// Both modified with same content - just update synced tree
    BothModifiedSame,
    /// Both deleted - remove from synced tree
    BothDeleted,
    /// CONFLICT: Both sides changed with different results
    Conflict(SyncConflictInfo),
}

/// Detailed information about a conflict
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct SyncConflictInfo {
    pub path_hash: FileId,
    /// Content hash of local version, None if deleted locally
    pub local_hash: Option<[u8; 32]>,
    /// Content hash of remote version, None if deleted on server
    pub remote_hash: Option<[u8; 32]>,
    /// Content hash of last synced version, None if file didn't exist before
    pub synced_hash: Option<[u8; 32]>,
    pub conflict_type: SyncConflictType,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl SyncConflictInfo {
    #[getter]
    fn path_hash(&self) -> String {
        hex::encode(self.path_hash)
    }

    #[getter]
    fn conflict_type(&self) -> SyncConflictType {
        self.conflict_type.clone()
    }
}

/// Type of conflict detected
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass(eq, eq_int))]
pub enum SyncConflictType {
    /// Both modified the same file differently
    ModifyModify,
    /// Local modified, remote deleted
    ModifyDelete,
    /// Local deleted, remote modified
    DeleteModify,
    /// Both created same path with different content
    CreateCreate,
}

/// User's choice for resolving a conflict
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass(eq, eq_int))]
pub enum SyncConflictResolution {
    /// Keep local version, overwrite server
    KeepLocal,
    /// Accept remote version, overwrite local
    AcceptRemote,
    /// Keep both - save local as .conflict file, download remote
    KeepBoth,
    /// Skip this conflict, handle later
    Skip,
}

/// Information about an upload conflict for resolution
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct SyncUploadConflict {
    /// File ID (path hash)
    pub file_id: FileId,
    /// Local file path (if known)
    pub local_path: Option<std::path::PathBuf>,
    /// Server's current revision ID
    pub server_revision_id: [u8; 32],
    /// Our base revision ID (what we thought server had)
    pub base_revision_id: Option<[u8; 32]>,
    /// Error message from server
    pub message: String,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl SyncUploadConflict {
    #[getter]
    fn file_id(&self) -> String {
        hex::encode(self.file_id)
    }

    #[getter]
    fn local_path(&self) -> Option<String> {
        self.local_path.as_ref().map(|p| p.display().to_string())
    }

    #[getter]
    fn server_revision_id(&self) -> String {
        hex::encode(self.server_revision_id)
    }

    #[getter]
    fn base_revision_id(&self) -> Option<String> {
        self.base_revision_id.map(hex::encode)
    }

    #[getter]
    fn message(&self) -> &str {
        &self.message
    }
}

/// Information about a plan conflict (from three-tree comparison)
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct SyncPlanConflict {
    /// File ID (path hash)
    pub file_id: FileId,
    /// Local file path (if known)
    pub local_path: Option<std::path::PathBuf>,
    /// Type of conflict
    pub conflict_type: SyncConflictType,
    /// Whether file exists locally
    pub has_local: bool,
    /// Whether file exists on server
    pub has_remote: bool,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl SyncPlanConflict {
    #[getter]
    fn file_id(&self) -> String {
        hex::encode(self.file_id)
    }

    #[getter]
    fn local_path(&self) -> Option<String> {
        self.local_path.as_ref().map(|p| p.display().to_string())
    }

    #[getter]
    fn conflict_type(&self) -> SyncConflictType {
        self.conflict_type.clone()
    }

    #[getter]
    fn has_local(&self) -> bool {
        self.has_local
    }

    #[getter]
    fn has_remote(&self) -> bool {
        self.has_remote
    }
}

/// Unified conflict type for resolution
#[derive(Debug, Clone)]
pub enum SyncConflict {
    /// Conflict detected during upload (server rejected due to stale revision)
    Upload(SyncUploadConflict),
    /// Conflict detected during plan phase (three-tree comparison)
    Plan(SyncPlanConflict),
}

/// Python-friendly wrapper for SyncConflict (Rust enums with data can't be pyclass directly)
#[cfg(feature = "python")]
#[pyo3::prelude::pyclass]
pub struct PySyncConflict {
    inner: SyncConflict,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl PySyncConflict {
    /// Returns "upload" or "plan"
    #[getter]
    fn kind(&self) -> &str {
        match &self.inner {
            SyncConflict::Upload(_) => "upload",
            SyncConflict::Plan(_) => "plan",
        }
    }

    /// Returns the upload conflict details, or None if this is a plan conflict
    #[getter]
    fn upload(&self) -> Option<SyncUploadConflict> {
        match &self.inner {
            SyncConflict::Upload(c) => Some(c.clone()),
            SyncConflict::Plan(_) => None,
        }
    }

    /// Returns the plan conflict details, or None if this is an upload conflict
    #[getter]
    fn plan(&self) -> Option<SyncPlanConflict> {
        match &self.inner {
            SyncConflict::Upload(_) => None,
            SyncConflict::Plan(c) => Some(c.clone()),
        }
    }
}

#[cfg(feature = "python")]
impl From<&SyncConflict> for PySyncConflict {
    fn from(conflict: &SyncConflict) -> Self {
        Self {
            inner: conflict.clone(),
        }
    }
}

/// Result of resolving a conflict - tracks what actions were taken
#[derive(Debug, Clone, Default)]
pub struct SyncConflictResolutionResult {
    /// Whether the conflict was resolved (false if skipped)
    pub resolved: bool,
    /// Number of files uploaded during resolution
    pub files_uploaded: usize,
    /// Number of files downloaded during resolution
    pub files_downloaded: usize,
    /// Number of files deleted locally during resolution
    pub files_deleted_locally: usize,
    /// Number of files deleted remotely during resolution
    pub files_deleted_remotely: usize,
}

impl SyncConflictResolutionResult {
    pub fn skipped() -> Self {
        Self::default()
    }

    pub fn uploaded() -> Self {
        Self {
            resolved: true,
            files_uploaded: 1,
            ..Default::default()
        }
    }

    pub fn downloaded() -> Self {
        Self {
            resolved: true,
            files_downloaded: 1,
            ..Default::default()
        }
    }

    pub fn deleted_locally() -> Self {
        Self {
            resolved: true,
            files_deleted_locally: 1,
            ..Default::default()
        }
    }

    pub fn deleted_remotely() -> Self {
        Self {
            resolved: true,
            files_deleted_remotely: 1,
            ..Default::default()
        }
    }

    /// Keep both: download remote + local saved as conflict copy
    pub fn kept_both() -> Self {
        Self {
            resolved: true,
            files_downloaded: 1,
            ..Default::default()
        }
    }
}

// =============================================================================
// Sync Mode
// =============================================================================

/// Sync mode hint for conflict handling.
///
/// Note: The actual conflict behavior is determined by the `conflict_resolver` callback
/// passed to `sync_with_resolver()`. This enum is kept for API compatibility and as a
/// hint to the CLI about which resolver to use:
/// - `Interactive`: CLI prompts user for each conflict
/// - `NonInteractive`: CLI uses a resolver that auto-skips conflicts
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum SyncMode {
    /// Interactive: CLI should prompt user for each conflict
    #[default]
    Interactive,
    /// Non-interactive: CLI should auto-resolve (typically skip) conflicts
    NonInteractive,
}

/// Cached filesystem metadata for mtime-based scan optimization.
/// Stored per-file in the synced tree to skip re-hashing unchanged files.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CachedFileInfo {
    /// File modification time (seconds since epoch)
    pub mtime_secs: u64,
    /// File size in bytes (filesystem-reported)
    pub size_bytes: u64,
}

/// The three-tree sync state maintained by the client
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncState {
    /// SS58 address of the user's Substrate/Bittensor account
    #[serde(alias = "user_id")]
    pub ss58_address: String,
    /// 16-char hex folder hash
    #[serde(default)]
    pub folder_hash: String,

    /// LOCAL TREE: Current state of files on disk
    /// Populated by scanning the filesystem
    pub local: FileTree,

    /// REMOTE TREE: Current state on the server
    /// Populated by fetching from server
    pub remote: FileTree,

    /// SYNCED TREE: Last known synchronized state (merge base)
    /// Updated ONLY after successful sync operations
    pub synced: FileTree,

    /// Mapping from path_hash to actual plaintext path (local only, never synced to server)
    #[serde(default, with = "hcfs_shared::utils::hex_key_pathbuf_map")]
    pub path_index: HashMap<FileId, PathBuf>,

    /// Last successful sync timestamp (Unix epoch seconds)
    pub last_sync_timestamp: u64,

    /// Cache of filesystem metadata (mtime, size) per file, keyed by path_hash.
    /// Used to skip re-hashing files that haven't changed on disk.
    #[serde(default, with = "hcfs_shared::utils::hex_key_map")]
    pub fs_cache: HashMap<FileId, CachedFileInfo>,

    /// Encrypted paths from remote (transient, for path reconstruction during download)
    /// Key is FileId (path_hash), value is encrypted path bytes
    #[serde(skip)]
    pub remote_encrypted_paths: HashMap<FileId, Vec<u8>>,

    /// Plaintext file names from remote (transient, for progress display during download)
    /// Key is FileId (path_hash), value is the original file name
    #[serde(skip)]
    pub remote_file_names: HashMap<FileId, String>,

    /// Arion CIDs from remote (content hashes from storage backend)
    /// Key is FileId (path_hash), value is the Arion CID string
    #[serde(default, with = "hcfs_shared::utils::hex_key_map")]
    pub remote_arion_hashes: HashMap<FileId, String>,
}

// =============================================================================
// Sync Plan
// =============================================================================

/// Result of analyzing sync state - the plan of operations to execute
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct SyncPlan {
    /// Files to upload (local creates + modifies)
    pub uploads: Vec<FileId>,
    /// Files to download (remote creates + modifies)
    pub downloads: Vec<FileId>,
    /// Files to delete locally (remote deletions)
    pub local_deletes: Vec<FileId>,
    /// Files to delete on server (local deletions)
    pub remote_deletes: Vec<FileId>,
    /// Files with conflicts requiring user resolution
    pub conflicts: Vec<SyncConflictInfo>,
    /// Files that need no action
    pub unchanged: Vec<FileId>,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl SyncPlan {
    #[getter]
    fn uploads(&self) -> Vec<String> {
        self.uploads.iter().map(hex::encode).collect()
    }

    #[getter]
    fn downloads(&self) -> Vec<String> {
        self.downloads.iter().map(hex::encode).collect()
    }

    #[getter]
    fn local_deletes(&self) -> Vec<String> {
        self.local_deletes.iter().map(hex::encode).collect()
    }

    #[getter]
    fn remote_deletes(&self) -> Vec<String> {
        self.remote_deletes.iter().map(hex::encode).collect()
    }

    #[getter]
    fn conflicts(&self) -> Vec<SyncConflictInfo> {
        self.conflicts.clone()
    }

    #[getter]
    fn unchanged(&self) -> Vec<String> {
        self.unchanged.iter().map(hex::encode).collect()
    }

    #[pyo3(name = "has_conflicts")]
    fn py_has_conflicts(&self) -> bool {
        SyncPlan::has_conflicts(self)
    }

    #[pyo3(name = "has_work")]
    fn py_has_work(&self) -> bool {
        SyncPlan::has_work(self)
    }

    #[pyo3(name = "total_operations")]
    fn py_total_operations(&self) -> usize {
        SyncPlan::total_operations(self)
    }
}

impl SyncPlan {
    /// Check if there are any conflicts that need resolution
    #[must_use]
    pub fn has_conflicts(&self) -> bool {
        !self.conflicts.is_empty()
    }

    /// Check if there are any operations to perform
    #[must_use]
    pub fn has_work(&self) -> bool {
        self.total_operations() > 0
    }

    /// Get total number of operations
    #[must_use]
    pub fn total_operations(&self) -> usize {
        self.uploads.len()
            + self.downloads.len()
            + self.local_deletes.len()
            + self.remote_deletes.len()
            + self.conflicts.len()
    }

    /// Analyze three trees and produce a sync plan
    ///
    /// The algorithm collects all file IDs across all three trees,
    /// then classifies each file based on its presence and content in each tree.
    ///
    /// # Arguments
    /// * `local` - Current filesystem state
    /// * `remote` - Current server state
    /// * `synced` - Last synchronized state (merge base)
    ///
    /// # Returns
    /// A `SyncPlan` containing lists of files to upload, download, delete, and conflicts
    pub fn build(local: &FileTree, remote: &FileTree, synced: &FileTree) -> Self {
        let mut plan = Self::default();

        // Three-phase iteration avoids HashSet allocation by using existing HashMap lookups

        // Phase 1: Process all files in local tree
        for (file_id, local_meta) in &local.files {
            let action = Self::classify_file(
                Some(local_meta),
                remote.get(file_id),
                synced.get(file_id),
                file_id,
            );
            Self::apply_action(&mut plan, *file_id, action);
        }

        // Phase 2: Files in remote but NOT in local
        for (file_id, remote_meta) in &remote.files {
            if local.files.contains_key(file_id) {
                continue; // Already processed in Phase 1
            }
            let action = Self::classify_file(None, Some(remote_meta), synced.get(file_id), file_id);
            Self::apply_action(&mut plan, *file_id, action);
        }

        // Phase 3: Files in synced but NOT in local or remote
        for (file_id, synced_meta) in &synced.files {
            if local.files.contains_key(file_id) || remote.files.contains_key(file_id) {
                continue; // Already processed
            }
            let action = Self::classify_file(None, None, Some(synced_meta), file_id);
            Self::apply_action(&mut plan, *file_id, action);
        }

        plan
    }

    /// Apply a classified sync action to the plan
    fn apply_action(plan: &mut Self, file_id: FileId, action: SyncAction) {
        match action {
            SyncAction::Unchanged | SyncAction::BothModifiedSame | SyncAction::BothDeleted => {
                plan.unchanged.push(file_id);
            }
            SyncAction::LocalCreate | SyncAction::LocalModify => plan.uploads.push(file_id),
            SyncAction::LocalDelete => plan.remote_deletes.push(file_id),
            SyncAction::RemoteCreate | SyncAction::RemoteModify => plan.downloads.push(file_id),
            SyncAction::RemoteDelete => plan.local_deletes.push(file_id),
            SyncAction::Conflict(info) => plan.conflicts.push(info),
        }
    }

    /// Classify a single file based on its presence/content in three trees
    ///
    /// Decision matrix:
    /// | Local | Remote | Synced | Action |
    /// |-------|--------|--------|--------|
    /// | A | A | A | Unchanged |
    /// | A | - | - | LocalCreate |
    /// | - | A | - | RemoteCreate |
    /// | A | B | A | RemoteModify (local matches synced, remote changed) |
    /// | A | A | B | LocalModify (remote matches synced, local changed) |
    /// | - | A | A | LocalDelete (local deleted, remote unchanged) |
    /// | A | - | A | RemoteDelete (remote deleted, local unchanged) |
    /// | A | B | C | CONFLICT (ModifyModify) |
    /// | A | - | B | CONFLICT (ModifyDelete) |
    /// | - | A | B | CONFLICT (DeleteModify) |
    /// | A | B | - | CONFLICT (CreateCreate) |
    ///
    /// Where A, B, C = different content hashes, `-` = not present
    fn classify_file(
        local: Option<&FileMetadata>,
        remote: Option<&FileMetadata>,
        synced: Option<&FileMetadata>,
        file_id: &FileId,
    ) -> SyncAction {
        match (local, remote, synced) {
            // =====================================================================
            // UNCHANGED CASES
            // =====================================================================

            // All three match - no changes
            (Some(l), Some(r), Some(s))
                if l.content_matches(r) && r.content_matches(s) && l.content_matches(s) =>
            {
                SyncAction::Unchanged
            }

            // Not in any tree - shouldn't happen but handle gracefully
            (None, None, None) => SyncAction::Unchanged,

            // =====================================================================
            // LOCAL-ONLY CHANGES
            // =====================================================================

            // Created locally (in local only, not in synced or remote)
            (Some(_), None, None) => SyncAction::LocalCreate,

            // Modified locally: local differs from synced, remote matches synced
            (Some(l), Some(r), Some(s)) if !l.content_matches(s) && r.content_matches(s) => {
                SyncAction::LocalModify
            }

            // Deleted locally: was in synced, gone from local, remote matches synced
            (None, Some(r), Some(s)) if r.content_matches(s) => SyncAction::LocalDelete,

            // =====================================================================
            // REMOTE-ONLY CHANGES
            // =====================================================================

            // Created remotely (in remote only, not in synced or local)
            (None, Some(_), None) => SyncAction::RemoteCreate,

            // Modified remotely: remote differs from synced, local matches synced
            (Some(l), Some(r), Some(s)) if l.content_matches(s) && !r.content_matches(s) => {
                SyncAction::RemoteModify
            }

            // Deleted remotely: was in synced, gone from remote, local matches synced
            (Some(l), None, Some(s)) if l.content_matches(s) => SyncAction::RemoteDelete,

            // =====================================================================
            // BOTH CHANGED (same result - no conflict)
            // =====================================================================

            // Both modified to same content
            (Some(l), Some(r), Some(_s)) if l.content_matches(r) => SyncAction::BothModifiedSame,

            // Both created with same content
            (Some(l), Some(r), None) if l.content_matches(r) => SyncAction::BothModifiedSame,

            // Both deleted
            (None, None, Some(_)) => SyncAction::BothDeleted,

            // =====================================================================
            // CONFLICTS
            // =====================================================================

            // Both modified differently (local != synced, remote != synced, local != remote)
            (Some(l), Some(r), Some(s))
                if !l.content_matches(s) && !r.content_matches(s) && !l.content_matches(r) =>
            {
                SyncAction::Conflict(SyncConflictInfo {
                    path_hash: *file_id,
                    local_hash: Some(l.salted_hash),
                    remote_hash: Some(r.salted_hash),
                    synced_hash: Some(s.salted_hash),
                    conflict_type: SyncConflictType::ModifyModify,
                })
            }

            // Local modified, remote deleted
            (Some(l), None, Some(s)) if !l.content_matches(s) => {
                SyncAction::Conflict(SyncConflictInfo {
                    path_hash: *file_id,
                    local_hash: Some(l.salted_hash),
                    remote_hash: None, // Deleted on server
                    synced_hash: Some(s.salted_hash),
                    conflict_type: SyncConflictType::ModifyDelete,
                })
            }

            // Local deleted, remote modified
            (None, Some(r), Some(s)) if !r.content_matches(s) => {
                SyncAction::Conflict(SyncConflictInfo {
                    path_hash: *file_id,
                    local_hash: None, // Deleted locally
                    remote_hash: Some(r.salted_hash),
                    synced_hash: Some(s.salted_hash),
                    conflict_type: SyncConflictType::DeleteModify,
                })
            }

            // Both created with different content (race condition on new file)
            (Some(l), Some(r), None) if !l.content_matches(r) => {
                SyncAction::Conflict(SyncConflictInfo {
                    path_hash: *file_id,
                    local_hash: Some(l.salted_hash),
                    remote_hash: Some(r.salted_hash),
                    synced_hash: None,
                    conflict_type: SyncConflictType::CreateCreate,
                })
            }

            // =====================================================================
            // EDGE CASES - treat conservatively
            // =====================================================================

            // File in local and synced only (remote somehow missing but synced has it)
            // This shouldn't happen in normal operation - treat as local modify
            (Some(_l), None, Some(_s)) => SyncAction::LocalModify,

            // File in remote and synced only (local somehow missing but synced has it)
            // This shouldn't happen in normal operation - treat as local delete
            (None, Some(_r), Some(_s)) => SyncAction::LocalDelete,

            // Fallback for any unhandled case - treat as conflict for safety
            // This should never be reached if the above cases are exhaustive
            _ => {
                let local_hash = local.map(|l| l.salted_hash);
                let remote_hash = remote.map(|r| r.salted_hash);
                let synced_hash = synced.map(|s| s.salted_hash);

                SyncAction::Conflict(SyncConflictInfo {
                    path_hash: *file_id,
                    local_hash,
                    remote_hash,
                    synced_hash,
                    conflict_type: SyncConflictType::ModifyModify,
                })
            }
        }
    }
}

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

    fn make_metadata(path: &str, content: &str) -> FileMetadata {
        let path_hash: [u8; 32] = blake3::hash(path.as_bytes()).into();
        let salted_hash: [u8; 32] = blake3::hash(content.as_bytes()).into();

        FileMetadata {
            path_hash,
            salted_hash,
            size_bytes: content.len() as u64,
            revision_seq: 1,
            revision_id: [0u8; 32],
            encryption_nonce: [0u8; 24],
        }
    }

    #[test]
    fn test_unchanged_all_match() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let meta = make_metadata("test.txt", "content");
        let file_id = meta.path_hash;

        local.insert(meta.clone());
        remote.insert(meta.clone());
        synced.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.unchanged.contains(&file_id));
        assert!(plan.uploads.is_empty());
        assert!(plan.downloads.is_empty());
        assert!(plan.conflicts.is_empty());
    }

    #[test]
    fn test_local_create() {
        let mut local = FileTree::default();
        let remote = FileTree::default();
        let synced = FileTree::default();

        let meta = make_metadata("new.txt", "new content");
        let file_id = meta.path_hash;
        local.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.uploads.contains(&file_id));
        assert!(plan.downloads.is_empty());
    }

    #[test]
    fn test_remote_create() {
        let local = FileTree::default();
        let mut remote = FileTree::default();
        let synced = FileTree::default();

        let meta = make_metadata("remote.txt", "remote content");
        let file_id = meta.path_hash;
        remote.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.downloads.contains(&file_id));
        assert!(plan.uploads.is_empty());
    }

    #[test]
    fn test_local_modify() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");
        let file_id = original.path_hash;

        let mut modified = original.clone();
        modified.salted_hash = [1u8; 32]; // Different content

        local.insert(modified);
        remote.insert(original.clone());
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.uploads.contains(&file_id));
    }

    #[test]
    fn test_remote_modify() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");
        let file_id = original.path_hash;

        let mut modified = original.clone();
        modified.salted_hash = [1u8; 32]; // Different content

        local.insert(original.clone());
        remote.insert(modified);
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.downloads.contains(&file_id));
    }

    #[test]
    fn test_local_delete() {
        let local = FileTree::default(); // File not present
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let meta = make_metadata("deleted.txt", "content");
        let file_id = meta.path_hash;

        remote.insert(meta.clone());
        synced.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.remote_deletes.contains(&file_id));
    }

    #[test]
    fn test_remote_delete() {
        let mut local = FileTree::default();
        let remote = FileTree::default(); // File not present
        let mut synced = FileTree::default();

        let meta = make_metadata("deleted.txt", "content");
        let file_id = meta.path_hash;

        local.insert(meta.clone());
        synced.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(plan.local_deletes.contains(&file_id));
    }

    #[test]
    fn test_conflict_modify_modify() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");
        let file_id = original.path_hash;

        let mut local_mod = original.clone();
        local_mod.salted_hash = [1u8; 32];

        let mut remote_mod = original.clone();
        remote_mod.salted_hash = [2u8; 32];

        local.insert(local_mod);
        remote.insert(remote_mod);
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert_eq!(plan.conflicts.len(), 1);
        assert_eq!(
            plan.conflicts[0].conflict_type,
            SyncConflictType::ModifyModify
        );
        assert_eq!(plan.conflicts[0].path_hash, file_id);
    }

    #[test]
    fn test_conflict_modify_delete() {
        let mut local = FileTree::default();
        let remote = FileTree::default(); // Deleted on remote
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");
        let file_id = original.path_hash;

        let mut modified = original.clone();
        modified.salted_hash = [1u8; 32];

        local.insert(modified);
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert_eq!(plan.conflicts.len(), 1);
        assert_eq!(
            plan.conflicts[0].conflict_type,
            SyncConflictType::ModifyDelete
        );
        assert_eq!(plan.conflicts[0].path_hash, file_id);
    }

    #[test]
    fn test_conflict_delete_modify() {
        let local = FileTree::default(); // Deleted locally
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");
        let file_id = original.path_hash;

        let mut modified = original.clone();
        modified.salted_hash = [1u8; 32];

        remote.insert(modified);
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert_eq!(plan.conflicts.len(), 1);
        assert_eq!(
            plan.conflicts[0].conflict_type,
            SyncConflictType::DeleteModify
        );
        assert_eq!(plan.conflicts[0].path_hash, file_id);
    }

    #[test]
    fn test_conflict_create_create() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let synced = FileTree::default(); // Not in synced

        let local_file = make_metadata("new.txt", "local content");
        let file_id = local_file.path_hash;

        let mut remote_file = local_file.clone();
        remote_file.salted_hash = [1u8; 32]; // Different content

        local.insert(local_file);
        remote.insert(remote_file);

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert_eq!(plan.conflicts.len(), 1);
        assert_eq!(
            plan.conflicts[0].conflict_type,
            SyncConflictType::CreateCreate
        );
        assert_eq!(plan.conflicts[0].path_hash, file_id);
    }

    #[test]
    fn test_both_modified_same() {
        let mut local = FileTree::default();
        let mut remote = FileTree::default();
        let mut synced = FileTree::default();

        let original = make_metadata("file.txt", "original");

        let mut modified = original.clone();
        modified.salted_hash = [1u8; 32];

        local.insert(modified.clone());
        remote.insert(modified.clone());
        synced.insert(original);

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Should be in unchanged, not conflicts
        assert!(plan.conflicts.is_empty());
        assert_eq!(plan.unchanged.len(), 1);
    }

    #[test]
    fn test_both_deleted() {
        let local = FileTree::default();
        let remote = FileTree::default();
        let mut synced = FileTree::default();

        let meta = make_metadata("deleted.txt", "content");
        synced.insert(meta);

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Should be in unchanged (both agree it's deleted)
        assert!(plan.conflicts.is_empty());
        assert_eq!(plan.unchanged.len(), 1);
    }
}

/// Extended tests for multi-client sync scenarios
///
/// These tests simulate edge cases that occur when multiple clients
/// sync to the same drive, including race conditions and offline scenarios.
#[cfg(test)]
mod multi_client_tests {
    use super::*;
    fn make_file(path: &str, content: &str, seq: u64) -> FileMetadata {
        let path_hash: [u8; 32] = blake3::hash(path.as_bytes()).into();
        let salted_hash: [u8; 32] = blake3::hash(content.as_bytes()).into();

        FileMetadata {
            path_hash,
            salted_hash,
            size_bytes: content.len() as u64,
            revision_seq: seq,
            revision_id: rand::random(),
            encryption_nonce: rand::random(),
        }
    }

    // =========================================================================
    // Multi-Client Race Conditions
    // =========================================================================

    /// Scenario: Client A and Client B both create the same file while offline.
    /// When they sync, both have the file in local but not in synced (first sync).
    /// The second client to sync should see a CreateCreate conflict.
    #[test]
    fn test_two_clients_create_same_file_offline() {
        // Client A's view after creating file locally
        let mut client_a_local = FileTree::default();
        let client_a_remote = FileTree::default(); // Empty, hasn't synced yet
        let client_a_synced = FileTree::default(); // Empty, first sync

        let file_a = make_file("notes.txt", "Client A content", 1);
        client_a_local.insert(file_a.clone());

        // Client A syncs first - should upload
        let plan_a = SyncPlan::build(&client_a_local, &client_a_remote, &client_a_synced);
        assert!(plan_a.uploads.contains(&file_a.path_hash));
        assert!(plan_a.conflicts.is_empty());

        // After Client A syncs, the file exists on server
        // Now Client B tries to sync with their version
        let mut client_b_local = FileTree::default();
        let mut client_b_remote = FileTree::default();
        let client_b_synced = FileTree::default(); // Empty, first sync for B

        let file_b = make_file("notes.txt", "Client B content", 1); // Same path, different content
        client_b_local.insert(file_b.clone());
        client_b_remote.insert(file_a.clone()); // Server has A's version

        // Client B should see CreateCreate conflict
        let plan_b = SyncPlan::build(&client_b_local, &client_b_remote, &client_b_synced);
        assert_eq!(plan_b.conflicts.len(), 1);
        assert_eq!(
            plan_b.conflicts[0].conflict_type,
            SyncConflictType::CreateCreate
        );
    }

    /// Scenario: Client A modifies file, Client B modifies same file differently.
    /// Both were online and had the same synced state, but made changes before syncing.
    #[test]
    fn test_concurrent_modifications() {
        let original = make_file("shared.txt", "original content", 1);

        // Both clients start with the same synced state
        let mut synced = FileTree::default();
        synced.insert(original.clone());

        // Client A modifies locally
        let mut client_a_local = FileTree::default();
        let modified_a = make_file("shared.txt", "Client A edits", 2);
        client_a_local.insert(modified_a.clone());

        // Client A syncs first (remote still has original)
        let mut remote_before_a = FileTree::default();
        remote_before_a.insert(original.clone());

        let plan_a = SyncPlan::build(&client_a_local, &remote_before_a, &synced);
        assert!(plan_a.uploads.contains(&modified_a.path_hash));
        assert!(plan_a.conflicts.is_empty());

        // After A syncs, remote has A's version
        let mut remote_after_a = FileTree::default();
        remote_after_a.insert(modified_a.clone());

        // Client B also modified (still has old synced state)
        let mut client_b_local = FileTree::default();
        let modified_b = make_file("shared.txt", "Client B edits", 2);
        client_b_local.insert(modified_b.clone());

        // Client B syncs - should see ModifyModify conflict
        let plan_b = SyncPlan::build(&client_b_local, &remote_after_a, &synced);
        assert_eq!(plan_b.conflicts.len(), 1);
        assert_eq!(
            plan_b.conflicts[0].conflict_type,
            SyncConflictType::ModifyModify
        );
    }

    /// Scenario: Client A deletes file, Client B modifies same file.
    #[test]
    fn test_delete_vs_modify_race() {
        let original = make_file("report.txt", "quarterly report", 1);

        let mut synced = FileTree::default();
        synced.insert(original.clone());

        // Client A deletes (local is empty)
        let client_a_local = FileTree::default();
        let mut client_a_remote = FileTree::default();
        client_a_remote.insert(original.clone());

        let plan_a = SyncPlan::build(&client_a_local, &client_a_remote, &synced);
        assert!(plan_a.remote_deletes.contains(&original.path_hash));

        // After A syncs, file is deleted on server
        let remote_after_a = FileTree::default();

        // Client B modified the file (doesn't know it's deleted)
        let mut client_b_local = FileTree::default();
        let modified_b = make_file("report.txt", "updated quarterly report", 2);
        client_b_local.insert(modified_b.clone());

        // Client B syncs - should see ModifyDelete conflict
        let plan_b = SyncPlan::build(&client_b_local, &remote_after_a, &synced);
        assert_eq!(plan_b.conflicts.len(), 1);
        assert_eq!(
            plan_b.conflicts[0].conflict_type,
            SyncConflictType::ModifyDelete
        );
    }

    // =========================================================================
    // Offline/Online Scenarios
    // =========================================================================

    /// Scenario: Client goes offline, makes many changes, comes back online
    /// to find the server has completely different state.
    #[test]
    fn test_long_offline_period_many_changes() {
        // Original synced state: files A, B, C
        let file_a = make_file("a.txt", "file a", 1);
        let file_b = make_file("b.txt", "file b", 1);
        let file_c = make_file("c.txt", "file c", 1);

        let mut synced = FileTree::default();
        synced.insert(file_a.clone());
        synced.insert(file_b.clone());
        synced.insert(file_c.clone());

        // Client goes offline and:
        // - Modifies A
        // - Deletes B
        // - Creates D
        let mut local = FileTree::default();
        let modified_a = make_file("a.txt", "modified file a", 2);
        let file_d = make_file("d.txt", "new file d", 1);
        local.insert(modified_a.clone());
        // B is not in local (deleted)
        local.insert(file_c.clone()); // C unchanged
        local.insert(file_d.clone());

        // Meanwhile on server:
        // - A is unchanged
        // - B is modified by another client
        // - C is deleted
        // - E is created
        let mut remote = FileTree::default();
        remote.insert(file_a.clone()); // A unchanged
        let modified_b = make_file("b.txt", "server modified b", 2);
        remote.insert(modified_b.clone());
        // C is deleted on server
        let file_e = make_file("e.txt", "server file e", 1);
        remote.insert(file_e.clone());

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Expected:
        // - A: local modified, remote unchanged → upload
        // - B: local deleted, remote modified → CONFLICT (DeleteModify)
        // - C: local unchanged, remote deleted → download (delete locally)
        // - D: local created → upload
        // - E: remote created → download

        assert!(plan.uploads.contains(&modified_a.path_hash)); // A
        assert!(plan.uploads.contains(&file_d.path_hash)); // D
        assert!(plan.downloads.contains(&file_e.path_hash)); // E
        assert!(plan.local_deletes.contains(&file_c.path_hash)); // C

        // B should be a DeleteModify conflict
        assert_eq!(plan.conflicts.len(), 1);
        assert_eq!(plan.conflicts[0].path_hash, file_b.path_hash);
        assert_eq!(
            plan.conflicts[0].conflict_type,
            SyncConflictType::DeleteModify
        );
    }

    /// Scenario: Client syncs, then immediately syncs again with no changes.
    /// Should result in no operations.
    #[test]
    fn test_immediate_resync_no_changes() {
        let file = make_file("stable.txt", "stable content", 1);

        let mut tree = FileTree::default();
        tree.insert(file.clone());

        // All three trees are identical (just synced)
        let plan = SyncPlan::build(&tree, &tree, &tree);

        assert!(plan.uploads.is_empty());
        assert!(plan.downloads.is_empty());
        assert!(plan.local_deletes.is_empty());
        assert!(plan.remote_deletes.is_empty());
        assert!(plan.conflicts.is_empty());
        assert!(!plan.unchanged.is_empty());
    }

    // =========================================================================
    // Edge Cases
    // =========================================================================

    /// Scenario: Empty sync (new client, empty server)
    #[test]
    fn test_empty_sync() {
        let local = FileTree::default();
        let remote = FileTree::default();
        let synced = FileTree::default();

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert!(!plan.has_work());
    }

    /// Scenario: First sync with many local files
    #[test]
    fn test_first_sync_many_files() {
        let mut local = FileTree::default();
        let remote = FileTree::default();
        let synced = FileTree::default();

        // Create 100 files locally
        for i in 0..100 {
            let file = make_file(&format!("file_{}.txt", i), &format!("content {}", i), 1);
            local.insert(file);
        }

        let plan = SyncPlan::build(&local, &remote, &synced);

        assert_eq!(plan.uploads.len(), 100);
        assert!(plan.downloads.is_empty());
        assert!(plan.conflicts.is_empty());
    }

    /// Scenario: Two files with same content but different paths
    /// Should NOT be confused as the same file
    #[test]
    fn test_same_content_different_paths() {
        let mut local = FileTree::default();
        let remote = FileTree::default();
        let synced = FileTree::default();

        // Two files with identical content but different paths
        let file_a = make_file("path/a.txt", "identical content", 1);
        let file_b = make_file("path/b.txt", "identical content", 1);

        local.insert(file_a.clone());
        local.insert(file_b.clone());

        // Neither exists on server or synced (first sync)
        let plan = SyncPlan::build(&local, &remote, &synced);

        // Both should be detected as LocalCreate and scheduled for upload
        // They have different path_hash even though same content
        assert_eq!(plan.uploads.len(), 2);
        assert!(plan.uploads.contains(&file_a.path_hash));
        assert!(plan.uploads.contains(&file_b.path_hash));
    }

    /// Scenario: File appears to be renamed (delete old + create new with same content)
    /// Current implementation doesn't detect renames, so this appears as delete + create
    #[test]
    fn test_file_rename_appears_as_delete_create() {
        let old_file = make_file("old_name.txt", "file content", 1);
        let new_file = make_file("new_name.txt", "file content", 1); // Same content!

        let mut synced = FileTree::default();
        synced.insert(old_file.clone());

        // Local: old file deleted, new file created
        let mut local = FileTree::default();
        local.insert(new_file.clone());

        // Remote: unchanged
        let mut remote = FileTree::default();
        remote.insert(old_file.clone());

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Should see: upload new_name.txt, delete old_name.txt from server
        assert!(plan.uploads.contains(&new_file.path_hash));
        assert!(plan.remote_deletes.contains(&old_file.path_hash));
        assert!(plan.conflicts.is_empty());
    }

    /// Scenario: Both clients make the exact same change (same content)
    /// Should NOT be a conflict
    #[test]
    fn test_both_make_identical_change() {
        let original = make_file("doc.txt", "original", 1);

        let mut synced = FileTree::default();
        synced.insert(original.clone());

        // Both clients modify to same content
        let modified = make_file("doc.txt", "both changed to this", 2);

        let mut local = FileTree::default();
        local.insert(modified.clone());

        let mut remote = FileTree::default();
        remote.insert(modified.clone());

        let plan = SyncPlan::build(&local, &remote, &synced);

        // No conflict - both made identical change
        assert!(plan.conflicts.is_empty());
        assert!(plan.unchanged.contains(&modified.path_hash));
    }

    /// Scenario: Client's synced tree is ahead of remote (shouldn't happen normally)
    /// This could occur if remote state was rolled back
    #[test]
    fn test_synced_ahead_of_remote_rollback() {
        let v1 = make_file("data.txt", "version 1", 1);
        let v2 = make_file("data.txt", "version 2", 2);

        // Synced has v2 (client synced successfully)
        let mut synced = FileTree::default();
        synced.insert(v2.clone());

        // Local has v2
        let mut local = FileTree::default();
        local.insert(v2.clone());

        // But remote was rolled back to v1!
        let mut remote = FileTree::default();
        remote.insert(v1.clone());

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Remote changed (rolled back), local unchanged
        // Should download the "older" version (remote is authoritative)
        assert!(plan.downloads.contains(&v1.path_hash));
    }

    // =========================================================================
    // Stress Test
    // =========================================================================

    /// Scenario: Large number of mixed operations
    #[test]
    fn test_large_mixed_operations() {
        let mut synced = FileTree::default();
        let mut local = FileTree::default();
        let mut remote = FileTree::default();

        // Create base state: 50 files in synced
        for i in 0..50 {
            let file = make_file(&format!("base_{}.txt", i), &format!("base {}", i), 1);
            synced.insert(file.clone());
            local.insert(file.clone());
            remote.insert(file.clone());
        }

        // Local changes:
        // - Modify first 10
        // - Delete 10-20
        // - Create 10 new files
        for i in 0..10 {
            let modified = make_file(
                &format!("base_{}.txt", i),
                &format!("local modified {}", i),
                2,
            );
            local.files.insert(modified.path_hash, modified);
        }
        for i in 10..20 {
            let file = make_file(&format!("base_{}.txt", i), &format!("base {}", i), 1);
            local.files.remove(&file.path_hash);
        }
        for i in 0..10 {
            let new_file = make_file(
                &format!("local_new_{}.txt", i),
                &format!("local new {}", i),
                1,
            );
            local.insert(new_file);
        }

        // Remote changes:
        // - Modify 5-15 (overlaps with local modifications and deletions)
        // - Delete 25-35
        // - Create 10 new files
        for i in 5..15 {
            let modified = make_file(
                &format!("base_{}.txt", i),
                &format!("remote modified {}", i),
                2,
            );
            remote.files.insert(modified.path_hash, modified);
        }
        for i in 25..35 {
            let file = make_file(&format!("base_{}.txt", i), &format!("base {}", i), 1);
            remote.files.remove(&file.path_hash);
        }
        for i in 0..10 {
            let new_file = make_file(
                &format!("remote_new_{}.txt", i),
                &format!("remote new {}", i),
                1,
            );
            remote.insert(new_file);
        }

        let plan = SyncPlan::build(&local, &remote, &synced);

        // Verify we got a mix of operations
        assert!(!plan.uploads.is_empty(), "Should have uploads");
        assert!(!plan.downloads.is_empty(), "Should have downloads");
        assert!(!plan.conflicts.is_empty(), "Should have conflicts");
        assert!(!plan.local_deletes.is_empty(), "Should have local deletes");
        assert!(
            !plan.remote_deletes.is_empty(),
            "Should have remote deletes"
        );

        // Conflicts should be in the overlap region (5-9 modified by both, 10-14 local delete vs remote modify)
        assert!(
            plan.conflicts.len() >= 5,
            "Should have at least 5 conflicts from overlap"
        );
    }
}
