mockforge_core/workspace/
sync.rs

1//! Synchronization functionality
2//!
3//! This module provides synchronization capabilities for workspaces,
4//! including conflict resolution, merge strategies, and sync status tracking.
5
6use crate::workspace::{EntityId, Workspace};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json;
10use serde_yaml;
11use std::collections::HashMap;
12use std::path::Path;
13use tokio::fs;
14
15/// Synchronization configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SyncConfig {
18    /// Whether synchronization is enabled
19    pub enabled: bool,
20    /// Synchronization provider (git, cloud, etc.)
21    pub provider: SyncProvider,
22    /// Synchronization interval in seconds
23    pub interval_seconds: u64,
24    /// Conflict resolution strategy
25    pub conflict_strategy: ConflictResolutionStrategy,
26    /// Whether to auto-commit changes
27    pub auto_commit: bool,
28    /// Whether to push changes automatically
29    pub auto_push: bool,
30    /// Directory structure preference
31    pub directory_structure: SyncDirectoryStructure,
32    /// Sync direction preference
33    pub sync_direction: SyncDirection,
34}
35
36/// Synchronization provider
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum SyncProvider {
39    /// Git-based synchronization
40    Git {
41        /// Repository URL
42        repo_url: String,
43        /// Branch name
44        branch: String,
45        /// Authentication token (optional)
46        auth_token: Option<String>,
47    },
48    /// Cloud-based synchronization
49    Cloud {
50        /// Service URL
51        service_url: String,
52        /// API key
53        api_key: String,
54        /// Project ID
55        project_id: String,
56    },
57    /// Local file system synchronization
58    Local {
59        /// Directory path
60        directory_path: String,
61        /// Watch for changes
62        watch_changes: bool,
63    },
64}
65
66/// Conflict resolution strategy
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ConflictResolutionStrategy {
69    /// Always use local version
70    LocalWins,
71    /// Always use remote version
72    RemoteWins,
73    /// Manual resolution required
74    Manual,
75    /// Use last modified timestamp
76    LastModified,
77}
78
79/// Directory structure for synchronization
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum SyncDirectoryStructure {
82    /// Single directory with all workspaces
83    SingleDirectory,
84    /// Separate directory per workspace
85    PerWorkspace,
86    /// Hierarchical structure based on folders
87    Hierarchical,
88}
89
90/// Synchronization direction
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum SyncDirection {
93    /// Bidirectional sync
94    Bidirectional,
95    /// Local to remote only
96    LocalToRemote,
97    /// Remote to local only
98    RemoteToLocal,
99}
100
101/// Synchronization status
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SyncStatus {
104    /// Last sync timestamp
105    pub last_sync: Option<DateTime<Utc>>,
106    /// Current sync state
107    pub state: SyncState,
108    /// Number of pending changes
109    pub pending_changes: usize,
110    /// Number of conflicts
111    pub conflicts: usize,
112    /// Last error message (if any)
113    pub last_error: Option<String>,
114}
115
116/// Current synchronization state
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum SyncState {
119    /// Not synchronized
120    NotSynced,
121    /// Currently syncing
122    Syncing,
123    /// Synchronized successfully
124    Synced,
125    /// Sync failed
126    SyncFailed,
127    /// Has conflicts
128    HasConflicts,
129}
130
131/// Synchronization result
132#[derive(Debug, Clone)]
133pub struct SyncResult {
134    /// Whether sync was successful
135    pub success: bool,
136    /// Number of files changed
137    pub changes_count: usize,
138    /// Conflicts that occurred
139    pub conflicts: Vec<SyncConflict>,
140    /// Error message (if failed)
141    pub error: Option<String>,
142}
143
144/// Conflict during synchronization
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SyncConflict {
147    /// Entity ID that has conflict
148    pub entity_id: EntityId,
149    /// Entity type (workspace, request, etc.)
150    pub entity_type: String,
151    /// Local version
152    pub local_version: serde_json::Value,
153    /// Remote version
154    pub remote_version: serde_json::Value,
155    /// Resolution strategy used
156    pub resolution: ConflictResolution,
157}
158
159/// Conflict resolution choice
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum ConflictResolution {
162    /// Use local version
163    Local,
164    /// Use remote version
165    Remote,
166    /// Manual resolution required
167    Manual,
168}
169
170/// Workspace synchronization manager
171#[derive(Debug, Clone)]
172pub struct WorkspaceSyncManager {
173    /// Synchronization configuration
174    config: SyncConfig,
175    /// Current sync status
176    status: SyncStatus,
177    /// Pending conflicts
178    conflicts: Vec<SyncConflict>,
179    /// Total number of sync operations performed
180    total_syncs: usize,
181    /// Number of successful syncs
182    successful_syncs: usize,
183    /// Number of failed syncs
184    failed_syncs: usize,
185    /// Total number of resolved conflicts
186    resolved_conflicts: usize,
187    /// Duration of last sync in milliseconds
188    last_sync_duration_ms: Option<u64>,
189}
190
191/// Synchronization event for tracking sync operations
192#[derive(Debug, Clone)]
193pub enum SyncEvent {
194    /// Sync operation started
195    Started,
196    /// Sync progress update with current and total items
197    Progress {
198        /// Current number of items processed
199        current: usize,
200        /// Total number of items to process
201        total: usize,
202    },
203    /// Sync completed successfully with result details
204    Completed(SyncResult),
205    /// Sync failed with error message
206    Failed(String),
207    /// Conflict detected during synchronization
208    ConflictDetected(SyncConflict),
209}
210
211impl WorkspaceSyncManager {
212    /// Create a new sync manager
213    pub fn new(config: SyncConfig) -> Self {
214        let status = SyncStatus {
215            last_sync: None,
216            state: SyncState::NotSynced,
217            pending_changes: 0,
218            conflicts: 0,
219            last_error: None,
220        };
221
222        Self {
223            config,
224            status,
225            conflicts: Vec::new(),
226            total_syncs: 0,
227            successful_syncs: 0,
228            failed_syncs: 0,
229            resolved_conflicts: 0,
230            last_sync_duration_ms: None,
231        }
232    }
233
234    /// Get the current sync configuration
235    pub fn get_config(&self) -> &SyncConfig {
236        &self.config
237    }
238
239    /// Update sync configuration
240    pub fn update_config(&mut self, config: SyncConfig) {
241        self.config = config;
242    }
243
244    /// Get current sync status
245    pub fn get_status(&self) -> &SyncStatus {
246        &self.status
247    }
248
249    /// Get pending conflicts
250    pub fn get_conflicts(&self) -> &[SyncConflict] {
251        &self.conflicts
252    }
253
254    /// Check if sync is enabled
255    pub fn is_enabled(&self) -> bool {
256        self.config.enabled
257    }
258
259    /// Sync a workspace
260    pub async fn sync_workspace(
261        &mut self,
262        workspace: &mut Workspace,
263    ) -> Result<SyncResult, String> {
264        if !self.config.enabled {
265            return Err("Synchronization is disabled".to_string());
266        }
267
268        // Track sync count
269        self.total_syncs += 1;
270
271        // Start timing
272        let start_time = std::time::Instant::now();
273
274        self.status.state = SyncState::Syncing;
275        self.status.last_error = None;
276
277        let result = match &self.config.provider {
278            SyncProvider::Git {
279                repo_url,
280                branch,
281                auth_token,
282            } => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
283            SyncProvider::Cloud {
284                service_url,
285                api_key,
286                project_id,
287            } => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
288            SyncProvider::Local {
289                directory_path,
290                watch_changes,
291            } => self.sync_with_local(workspace, directory_path, *watch_changes).await,
292        };
293
294        // Calculate duration
295        let duration = start_time.elapsed();
296        let duration_ms = duration.as_millis() as u64;
297        self.last_sync_duration_ms = Some(duration_ms);
298
299        match &result {
300            Ok(sync_result) => {
301                if sync_result.success {
302                    self.successful_syncs += 1;
303                    self.status.state = SyncState::Synced;
304                    self.status.last_sync = Some(Utc::now());
305                    self.status.pending_changes = 0;
306                    self.status.conflicts = sync_result.conflicts.len();
307                } else {
308                    self.failed_syncs += 1;
309                    self.status.state = SyncState::SyncFailed;
310                    self.status.last_error = sync_result.error.clone();
311                }
312            }
313            Err(error) => {
314                self.failed_syncs += 1;
315                self.status.state = SyncState::SyncFailed;
316                self.status.last_error = Some(error.clone());
317            }
318        }
319
320        result
321    }
322
323    /// Sync with Git provider
324    async fn sync_with_git(
325        &self,
326        workspace: &mut Workspace,
327        repo_url: &str,
328        branch: &str,
329        auth_token: Option<&str>,
330    ) -> Result<SyncResult, String> {
331        // Create a temporary directory for the Git repository
332        let temp_dir =
333            tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
334
335        let repo_path = temp_dir.path().join("repo");
336
337        match self.config.sync_direction {
338            SyncDirection::LocalToRemote => {
339                self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
340                    .await
341            }
342            SyncDirection::RemoteToLocal => {
343                self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
344                    .await
345            }
346            SyncDirection::Bidirectional => {
347                self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
348                    .await
349            }
350        }
351    }
352
353    /// Sync local workspace to Git repository
354    async fn sync_local_to_git(
355        &self,
356        workspace: &Workspace,
357        repo_url: &str,
358        branch: &str,
359        auth_token: Option<&str>,
360        repo_path: &std::path::Path,
361    ) -> Result<SyncResult, String> {
362        // Clone or ensure repository exists
363        self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
364
365        // Serialize workspace to YAML file
366        let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
367        let workspace_yaml = serde_yaml::to_string(workspace)
368            .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
369
370        tokio::fs::write(&workspace_file, &workspace_yaml)
371            .await
372            .map_err(|e| format!("Failed to write workspace file: {}", e))?;
373
374        // Add, commit, and push changes
375        self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
376
377        Ok(SyncResult {
378            success: true,
379            changes_count: 1,
380            conflicts: vec![],
381            error: None,
382        })
383    }
384
385    /// Sync Git repository to local workspace
386    async fn sync_git_to_local(
387        &self,
388        workspace: &mut Workspace,
389        repo_url: &str,
390        branch: &str,
391        auth_token: Option<&str>,
392        repo_path: &std::path::Path,
393    ) -> Result<SyncResult, String> {
394        // Clone or pull repository
395        self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
396
397        // Read workspace from Git repository
398        let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
399
400        if !workspace_file.exists() {
401            return Ok(SyncResult {
402                success: true,
403                changes_count: 0,
404                conflicts: vec![],
405                error: None,
406            });
407        }
408
409        let workspace_yaml = tokio::fs::read_to_string(&workspace_file)
410            .await
411            .map_err(|e| format!("Failed to read workspace file: {}", e))?;
412
413        let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
414            .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
415
416        // Check for conflicts
417        let conflicts = self.detect_conflicts(workspace, &remote_workspace);
418
419        if conflicts.is_empty() {
420            // No conflicts, update local workspace
421            *workspace = remote_workspace;
422            Ok(SyncResult {
423                success: true,
424                changes_count: 1,
425                conflicts: vec![],
426                error: None,
427            })
428        } else {
429            // Conflicts exist
430            Ok(SyncResult {
431                success: true,
432                changes_count: 0,
433                conflicts,
434                error: None,
435            })
436        }
437    }
438
439    /// Bidirectional sync with Git repository
440    async fn sync_bidirectional_git(
441        &self,
442        workspace: &mut Workspace,
443        repo_url: &str,
444        branch: &str,
445        auth_token: Option<&str>,
446        repo_path: &std::path::Path,
447    ) -> Result<SyncResult, String> {
448        // First sync from Git to local
449        let pull_result = self
450            .sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
451            .await?;
452
453        if !pull_result.conflicts.is_empty() {
454            // Conflicts detected, return them
455            return Ok(pull_result);
456        }
457
458        // No conflicts, sync local to Git
459        self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
460    }
461
462    /// Ensure Git repository exists and is up to date
463    async fn ensure_git_repo(
464        &self,
465        repo_url: &str,
466        branch: &str,
467        auth_token: Option<&str>,
468        repo_path: &std::path::Path,
469    ) -> Result<(), String> {
470        use std::process::Command;
471
472        // Convert path to string using to_string_lossy for cross-platform compatibility
473        let repo_path_str = repo_path.to_string_lossy();
474
475        if repo_path.exists() {
476            // Repository exists, pull latest changes
477            let output = Command::new("git")
478                .args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
479                .output()
480                .map_err(|e| format!("Failed to pull repository: {}", e))?;
481
482            if !output.status.success() {
483                let stderr = String::from_utf8_lossy(&output.stderr);
484                return Err(format!("Git pull failed: {}", stderr));
485            }
486        } else {
487            // Clone repository
488            // If auth token provided, modify URL for authentication
489            let clone_url = if let Some(token) = auth_token {
490                self.inject_auth_token_into_url(repo_url, token)
491            } else {
492                repo_url.to_string()
493            };
494
495            let output = Command::new("git")
496                .args([
497                    "clone",
498                    "--branch",
499                    branch,
500                    &clone_url,
501                    repo_path_str.as_ref(),
502                ])
503                .output()
504                .map_err(|e| format!("Failed to clone repository: {}", e))?;
505
506            if !output.status.success() {
507                let stderr = String::from_utf8_lossy(&output.stderr);
508                return Err(format!("Git clone failed: {}", stderr));
509            }
510        }
511
512        Ok(())
513    }
514
515    /// Add, commit, and push changes to Git repository
516    async fn git_add_commit_push(
517        &self,
518        repo_path: &std::path::Path,
519        workspace_file: &std::path::Path,
520        _auth_token: Option<&str>,
521    ) -> Result<(), String> {
522        use std::process::Command;
523
524        // Use to_string_lossy for safe cross-platform path conversion
525        let repo_path_str = repo_path.to_string_lossy();
526
527        // Calculate relative path for git commands (works across platforms)
528        let file_path_str = workspace_file
529            .strip_prefix(repo_path)
530            .unwrap_or(workspace_file)
531            .to_string_lossy();
532
533        // Add file
534        let output = Command::new("git")
535            .args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
536            .output()
537            .map_err(|e| format!("Failed to add file to git: {}", e))?;
538
539        if !output.status.success() {
540            let stderr = String::from_utf8_lossy(&output.stderr);
541            return Err(format!("Git add failed: {}", stderr));
542        }
543
544        // Check if there are changes to commit
545        let status_output = Command::new("git")
546            .args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
547            .output()
548            .map_err(|e| format!("Failed to check git status: {}", e))?;
549
550        if status_output.stdout.is_empty() {
551            // No changes to commit
552            return Ok(());
553        }
554
555        // Commit changes
556        let output = Command::new("git")
557            .args([
558                "-C",
559                repo_path_str.as_ref(),
560                "commit",
561                "-m",
562                "Update workspace",
563            ])
564            .output()
565            .map_err(|e| format!("Failed to commit changes: {}", e))?;
566
567        if !output.status.success() {
568            let stderr = String::from_utf8_lossy(&output.stderr);
569            return Err(format!("Git commit failed: {}", stderr));
570        }
571
572        // Push changes
573        let output = Command::new("git")
574            .args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
575            .output()
576            .map_err(|e| format!("Failed to push changes: {}", e))?;
577
578        if !output.status.success() {
579            let stderr = String::from_utf8_lossy(&output.stderr);
580            return Err(format!("Git push failed: {}", stderr));
581        }
582
583        Ok(())
584    }
585
586    /// Inject authentication token into Git URL
587    fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
588        if let Some(https_pos) = url.find("https://") {
589            let rest = &url[https_pos + "https://".len()..];
590            format!("https://oauth2:{}@{}", token, rest)
591        } else {
592            // For SSH URLs or other formats, return as-is
593            url.to_string()
594        }
595    }
596
597    /// Sync with cloud provider
598    async fn sync_with_cloud(
599        &self,
600        workspace: &mut Workspace,
601        service_url: &str,
602        api_key: &str,
603        project_id: &str,
604    ) -> Result<SyncResult, String> {
605        // Create HTTP client
606        let client = reqwest::Client::new();
607
608        // Build API URLs
609        let base_url = service_url.trim_end_matches('/');
610        let workspace_url =
611            format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
612
613        match self.config.sync_direction {
614            SyncDirection::LocalToRemote => {
615                // Only upload local workspace to cloud
616                self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
617                    .await
618            }
619            SyncDirection::RemoteToLocal => {
620                // Only download remote workspace and update local
621                self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
622                    .await
623            }
624            SyncDirection::Bidirectional => {
625                // Fetch remote, compare, handle conflicts, then upload if needed
626                self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
627            }
628        }
629    }
630
631    /// Upload workspace to cloud service
632    async fn upload_workspace_to_cloud(
633        &self,
634        client: &reqwest::Client,
635        workspace_url: &str,
636        api_key: &str,
637        workspace: &Workspace,
638    ) -> Result<SyncResult, String> {
639        // Serialize workspace to JSON
640        let workspace_json = serde_json::to_string(workspace)
641            .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
642
643        // Upload to cloud
644        let response = client
645            .put(workspace_url)
646            .header("Authorization", format!("Bearer {}", api_key))
647            .header("Content-Type", "application/json")
648            .body(workspace_json)
649            .send()
650            .await
651            .map_err(|e| format!("Failed to upload workspace: {}", e))?;
652
653        if !response.status().is_success() {
654            let status = response.status();
655            let error_text = response.text().await.unwrap_or_default();
656            return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
657        }
658
659        Ok(SyncResult {
660            success: true,
661            changes_count: 1,
662            conflicts: vec![],
663            error: None,
664        })
665    }
666
667    /// Download workspace from cloud service
668    async fn download_workspace_from_cloud(
669        &self,
670        client: &reqwest::Client,
671        workspace_url: &str,
672        api_key: &str,
673        workspace: &mut Workspace,
674    ) -> Result<SyncResult, String> {
675        // Download from cloud
676        let response = client
677            .get(workspace_url)
678            .header("Authorization", format!("Bearer {}", api_key))
679            .send()
680            .await
681            .map_err(|e| format!("Failed to download workspace: {}", e))?;
682
683        if response.status() == reqwest::StatusCode::NOT_FOUND {
684            // Workspace doesn't exist in cloud, nothing to sync
685            return Ok(SyncResult {
686                success: true,
687                changes_count: 0,
688                conflicts: vec![],
689                error: None,
690            });
691        }
692
693        if !response.status().is_success() {
694            let status = response.status();
695            let error_text = response.text().await.unwrap_or_default();
696            return Err(format!("Cloud download failed with status {}: {}", status, error_text));
697        }
698
699        let remote_json: serde_json::Value = response
700            .json()
701            .await
702            .map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
703
704        // Deserialize remote workspace
705        let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
706            .map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
707
708        // Check for conflicts based on timestamps
709        let conflicts = self.detect_conflicts(workspace, &remote_workspace);
710
711        // Apply conflict resolution
712        if conflicts.is_empty() {
713            // No conflicts, update local workspace with remote
714            *workspace = remote_workspace;
715            Ok(SyncResult {
716                success: true,
717                changes_count: 1,
718                conflicts: vec![],
719                error: None,
720            })
721        } else {
722            // Conflicts exist, return them for manual resolution
723            Ok(SyncResult {
724                success: true,
725                changes_count: 0,
726                conflicts,
727                error: None,
728            })
729        }
730    }
731
732    /// Perform bidirectional synchronization
733    async fn bidirectional_sync(
734        &self,
735        client: &reqwest::Client,
736        workspace_url: &str,
737        api_key: &str,
738        workspace: &mut Workspace,
739    ) -> Result<SyncResult, String> {
740        // First try to download remote workspace
741        let download_result = self
742            .download_workspace_from_cloud(client, workspace_url, api_key, workspace)
743            .await?;
744
745        if !download_result.conflicts.is_empty() {
746            // Conflicts detected, return them
747            return Ok(download_result);
748        }
749
750        // No conflicts, upload local workspace
751        self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
752    }
753
754    /// Detect conflicts between local and remote workspaces
755    fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
756        let mut conflicts = vec![];
757
758        // Simple conflict detection based on updated_at timestamps
759        if local.updated_at > remote.updated_at {
760            // Local is newer, potential conflict
761            let local_json = serde_json::to_value(local).unwrap_or_default();
762            let remote_json = serde_json::to_value(remote).unwrap_or_default();
763
764            if local_json != remote_json {
765                conflicts.push(SyncConflict {
766                    entity_id: local.id.clone(),
767                    entity_type: "workspace".to_string(),
768                    local_version: local_json,
769                    remote_version: remote_json,
770                    resolution: ConflictResolution::Manual,
771                });
772            }
773        }
774
775        conflicts
776    }
777
778    /// Sync with local filesystem
779    async fn sync_with_local(
780        &self,
781        workspace: &mut Workspace,
782        directory_path: &str,
783        _watch_changes: bool,
784    ) -> Result<SyncResult, String> {
785        let dir_path = Path::new(directory_path);
786
787        // Ensure directory exists
788        if !dir_path.exists() {
789            fs::create_dir_all(dir_path)
790                .await
791                .map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
792        }
793
794        match self.config.sync_direction {
795            SyncDirection::LocalToRemote => {
796                // Write workspace to file
797                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
798                let content = serde_yaml::to_string(workspace)
799                    .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
800
801                fs::write(&file_path, content)
802                    .await
803                    .map_err(|e| format!("Failed to write workspace file: {}", e))?;
804
805                Ok(SyncResult {
806                    success: true,
807                    changes_count: 1,
808                    conflicts: vec![],
809                    error: None,
810                })
811            }
812            SyncDirection::RemoteToLocal => {
813                // Load workspace from file
814                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
815
816                if !file_path.exists() {
817                    return Err(format!("Workspace file not found: {:?}", file_path));
818                }
819
820                let content = fs::read_to_string(&file_path)
821                    .await
822                    .map_err(|e| format!("Failed to read workspace file: {}", e))?;
823
824                let remote_workspace: Workspace = serde_yaml::from_str(&content)
825                    .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
826
827                // Compare workspaces and detect conflicts
828                let conflicts = {
829                    let mut conflicts = vec![];
830
831                    // Check for conflicts
832                    if workspace.updated_at > remote_workspace.updated_at {
833                        // Local is newer, this is a conflict
834                        let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
835                        let remote_json =
836                            serde_json::to_value(&remote_workspace).unwrap_or_default();
837                        conflicts.push(SyncConflict {
838                            entity_id: workspace.id.clone(),
839                            entity_type: "workspace".to_string(),
840                            local_version: local_json,
841                            remote_version: remote_json,
842                            resolution: ConflictResolution::Manual,
843                        });
844                    } else if workspace.updated_at == remote_workspace.updated_at {
845                        // Same timestamp, check if content differs
846                        let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
847                        let remote_json =
848                            serde_json::to_value(&remote_workspace).unwrap_or_default();
849                        if local_json != remote_json {
850                            // Content differs but timestamps are same, conflict
851                            conflicts.push(SyncConflict {
852                                entity_id: workspace.id.clone(),
853                                entity_type: "workspace".to_string(),
854                                local_version: local_json,
855                                remote_version: remote_json,
856                                resolution: ConflictResolution::Manual,
857                            });
858                        }
859                    }
860
861                    conflicts
862                };
863
864                // If no conflicts and remote is newer or equal, update local workspace with remote
865                if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
866                    *workspace = remote_workspace;
867                    Ok(SyncResult {
868                        success: true,
869                        changes_count: 1,
870                        conflicts: vec![],
871                        error: None,
872                    })
873                } else {
874                    Ok(SyncResult {
875                        success: true,
876                        changes_count: 0,
877                        conflicts,
878                        error: None,
879                    })
880                }
881            }
882            SyncDirection::Bidirectional => {
883                // For bidirectional, first try to load remote, then write local
884                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
885
886                let mut conflicts = vec![];
887
888                if file_path.exists() {
889                    let content = fs::read_to_string(&file_path)
890                        .await
891                        .map_err(|e| format!("Failed to read workspace file: {}", e))?;
892
893                    let remote_workspace: Workspace = serde_yaml::from_str(&content)
894                        .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
895
896                    // Simple conflict detection based on updated_at
897                    if remote_workspace.updated_at > workspace.updated_at {
898                        // Remote is newer, this would be a conflict
899                        let remote_version =
900                            serde_json::to_value(&remote_workspace).unwrap_or_default();
901                        conflicts.push(SyncConflict {
902                            entity_id: workspace.id.clone(),
903                            entity_type: "workspace".to_string(),
904                            local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
905                            remote_version,
906                            resolution: ConflictResolution::Manual,
907                        });
908                    }
909                }
910
911                // Write local workspace
912                let content = serde_yaml::to_string(workspace)
913                    .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
914
915                fs::write(&file_path, content)
916                    .await
917                    .map_err(|e| format!("Failed to write workspace file: {}", e))?;
918
919                Ok(SyncResult {
920                    success: true,
921                    changes_count: 1,
922                    conflicts,
923                    error: None,
924                })
925            }
926        }
927    }
928
929    /// Resolve conflicts
930    pub fn resolve_conflicts(
931        &mut self,
932        resolutions: HashMap<EntityId, ConflictResolution>,
933    ) -> Result<usize, String> {
934        let mut resolved_count = 0;
935
936        for conflict in &self.conflicts.clone() {
937            if let Some(resolution) = resolutions.get(&conflict.entity_id) {
938                match resolution {
939                    ConflictResolution::Local => {
940                        // Apply local version
941                        resolved_count += 1;
942                    }
943                    ConflictResolution::Remote => {
944                        // Apply remote version
945                        resolved_count += 1;
946                    }
947                    ConflictResolution::Manual => {
948                        // Mark for manual resolution
949                        continue;
950                    }
951                }
952            }
953        }
954
955        // Track resolved conflicts
956        self.resolved_conflicts += resolved_count;
957
958        // Remove resolved conflicts
959        self.conflicts.retain(|conflict| {
960            !resolutions.contains_key(&conflict.entity_id)
961                || matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
962        });
963
964        self.status.conflicts = self.conflicts.len();
965        if self.conflicts.is_empty() {
966            self.status.state = SyncState::Synced;
967        } else {
968            self.status.state = SyncState::HasConflicts;
969        }
970
971        Ok(resolved_count)
972    }
973
974    /// Get sync statistics
975    pub fn get_sync_stats(&self) -> SyncStats {
976        SyncStats {
977            total_syncs: self.total_syncs,
978            successful_syncs: self.successful_syncs,
979            failed_syncs: self.failed_syncs,
980            total_conflicts: self.conflicts.len(),
981            resolved_conflicts: self.resolved_conflicts,
982            last_sync_duration_ms: self.last_sync_duration_ms,
983        }
984    }
985
986    /// Export sync configuration
987    pub fn export_config(&self) -> Result<String, String> {
988        serde_json::to_string_pretty(&self.config)
989            .map_err(|e| format!("Failed to serialize sync config: {}", e))
990    }
991
992    /// Import sync configuration
993    pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
994        let config: SyncConfig = serde_json::from_str(json_data)
995            .map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
996
997        self.config = config;
998        Ok(())
999    }
1000
1001    /// Check if there are pending changes
1002    pub fn has_pending_changes(&self) -> bool {
1003        self.status.pending_changes > 0
1004    }
1005
1006    /// Get conflicts that need manual resolution
1007    pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
1008        self.conflicts
1009            .iter()
1010            .filter(|_conflict| {
1011                // This would need to be determined based on the conflict resolution strategy
1012                true
1013            })
1014            .collect()
1015    }
1016}
1017
1018/// Synchronization statistics
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct SyncStats {
1021    /// Total number of sync operations
1022    pub total_syncs: usize,
1023    /// Number of successful syncs
1024    pub successful_syncs: usize,
1025    /// Number of failed syncs
1026    pub failed_syncs: usize,
1027    /// Total number of conflicts encountered
1028    pub total_conflicts: usize,
1029    /// Number of resolved conflicts
1030    pub resolved_conflicts: usize,
1031    /// Duration of last sync in milliseconds
1032    pub last_sync_duration_ms: Option<u64>,
1033}
1034
1035impl Default for SyncConfig {
1036    fn default() -> Self {
1037        Self {
1038            enabled: false,
1039            provider: SyncProvider::Local {
1040                directory_path: "./workspaces".to_string(),
1041                watch_changes: true,
1042            },
1043            interval_seconds: 300,
1044            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1045            auto_commit: true,
1046            auto_push: false,
1047            directory_structure: SyncDirectoryStructure::PerWorkspace,
1048            sync_direction: SyncDirection::Bidirectional,
1049        }
1050    }
1051}
1052
1053impl Default for WorkspaceSyncManager {
1054    fn default() -> Self {
1055        Self::new(SyncConfig::default())
1056    }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062    use serde_json::json;
1063
1064    #[test]
1065    fn test_sync_config_creation() {
1066        let config = SyncConfig {
1067            enabled: true,
1068            provider: SyncProvider::Local {
1069                directory_path: "/tmp/sync".to_string(),
1070                watch_changes: true,
1071            },
1072            interval_seconds: 60,
1073            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1074            auto_commit: true,
1075            auto_push: false,
1076            directory_structure: SyncDirectoryStructure::PerWorkspace,
1077            sync_direction: SyncDirection::Bidirectional,
1078        };
1079
1080        assert!(config.enabled);
1081        assert_eq!(config.interval_seconds, 60);
1082        assert!(config.auto_commit);
1083        assert!(!config.auto_push);
1084    }
1085
1086    #[test]
1087    fn test_sync_provider_git() {
1088        let provider = SyncProvider::Git {
1089            repo_url: "https://github.com/user/repo.git".to_string(),
1090            branch: "main".to_string(),
1091            auth_token: Some("token123".to_string()),
1092        };
1093
1094        match provider {
1095            SyncProvider::Git {
1096                repo_url,
1097                branch,
1098                auth_token,
1099            } => {
1100                assert_eq!(repo_url, "https://github.com/user/repo.git");
1101                assert_eq!(branch, "main");
1102                assert_eq!(auth_token, Some("token123".to_string()));
1103            }
1104            _ => panic!("Expected Git provider"),
1105        }
1106    }
1107
1108    #[test]
1109    fn test_sync_provider_cloud() {
1110        let provider = SyncProvider::Cloud {
1111            service_url: "https://api.example.com".to_string(),
1112            api_key: "key123".to_string(),
1113            project_id: "proj-456".to_string(),
1114        };
1115
1116        match provider {
1117            SyncProvider::Cloud {
1118                service_url,
1119                api_key,
1120                project_id,
1121            } => {
1122                assert_eq!(service_url, "https://api.example.com");
1123                assert_eq!(api_key, "key123");
1124                assert_eq!(project_id, "proj-456");
1125            }
1126            _ => panic!("Expected Cloud provider"),
1127        }
1128    }
1129
1130    #[test]
1131    fn test_sync_provider_local() {
1132        let provider = SyncProvider::Local {
1133            directory_path: "/tmp/sync".to_string(),
1134            watch_changes: true,
1135        };
1136
1137        match provider {
1138            SyncProvider::Local {
1139                directory_path,
1140                watch_changes,
1141            } => {
1142                assert_eq!(directory_path, "/tmp/sync");
1143                assert!(watch_changes);
1144            }
1145            _ => panic!("Expected Local provider"),
1146        }
1147    }
1148
1149    #[test]
1150    fn test_conflict_resolution_strategy_variants() {
1151        let local_wins = ConflictResolutionStrategy::LocalWins;
1152        let remote_wins = ConflictResolutionStrategy::RemoteWins;
1153        let manual = ConflictResolutionStrategy::Manual;
1154        let last_modified = ConflictResolutionStrategy::LastModified;
1155
1156        // Just verify they can be created
1157        match local_wins {
1158            ConflictResolutionStrategy::LocalWins => {}
1159            _ => panic!(),
1160        }
1161        match remote_wins {
1162            ConflictResolutionStrategy::RemoteWins => {}
1163            _ => panic!(),
1164        }
1165        match manual {
1166            ConflictResolutionStrategy::Manual => {}
1167            _ => panic!(),
1168        }
1169        match last_modified {
1170            ConflictResolutionStrategy::LastModified => {}
1171            _ => panic!(),
1172        }
1173    }
1174
1175    #[test]
1176    fn test_sync_directory_structure_variants() {
1177        let single = SyncDirectoryStructure::SingleDirectory;
1178        let per_workspace = SyncDirectoryStructure::PerWorkspace;
1179        let hierarchical = SyncDirectoryStructure::Hierarchical;
1180
1181        match single {
1182            SyncDirectoryStructure::SingleDirectory => {}
1183            _ => panic!(),
1184        }
1185        match per_workspace {
1186            SyncDirectoryStructure::PerWorkspace => {}
1187            _ => panic!(),
1188        }
1189        match hierarchical {
1190            SyncDirectoryStructure::Hierarchical => {}
1191            _ => panic!(),
1192        }
1193    }
1194
1195    #[test]
1196    fn test_sync_direction_variants() {
1197        let bidirectional = SyncDirection::Bidirectional;
1198        let local_to_remote = SyncDirection::LocalToRemote;
1199        let remote_to_local = SyncDirection::RemoteToLocal;
1200
1201        match bidirectional {
1202            SyncDirection::Bidirectional => {}
1203            _ => panic!(),
1204        }
1205        match local_to_remote {
1206            SyncDirection::LocalToRemote => {}
1207            _ => panic!(),
1208        }
1209        match remote_to_local {
1210            SyncDirection::RemoteToLocal => {}
1211            _ => panic!(),
1212        }
1213    }
1214
1215    #[test]
1216    fn test_sync_state_variants() {
1217        let not_synced = SyncState::NotSynced;
1218        let syncing = SyncState::Syncing;
1219        let synced = SyncState::Synced;
1220        let sync_failed = SyncState::SyncFailed;
1221        let has_conflicts = SyncState::HasConflicts;
1222
1223        match not_synced {
1224            SyncState::NotSynced => {}
1225            _ => panic!(),
1226        }
1227        match syncing {
1228            SyncState::Syncing => {}
1229            _ => panic!(),
1230        }
1231        match synced {
1232            SyncState::Synced => {}
1233            _ => panic!(),
1234        }
1235        match sync_failed {
1236            SyncState::SyncFailed => {}
1237            _ => panic!(),
1238        }
1239        match has_conflicts {
1240            SyncState::HasConflicts => {}
1241            _ => panic!(),
1242        }
1243    }
1244
1245    #[test]
1246    fn test_sync_status_creation() {
1247        let status = SyncStatus {
1248            last_sync: Some(Utc::now()),
1249            state: SyncState::Synced,
1250            pending_changes: 5,
1251            conflicts: 2,
1252            last_error: Some("Test error".to_string()),
1253        };
1254
1255        assert!(status.last_sync.is_some());
1256        match status.state {
1257            SyncState::Synced => {}
1258            _ => panic!(),
1259        }
1260        assert_eq!(status.pending_changes, 5);
1261        assert_eq!(status.conflicts, 2);
1262        assert_eq!(status.last_error, Some("Test error".to_string()));
1263    }
1264
1265    #[test]
1266    fn test_sync_result_creation() {
1267        let result = SyncResult {
1268            success: true,
1269            changes_count: 10,
1270            conflicts: vec![],
1271            error: None,
1272        };
1273
1274        assert!(result.success);
1275        assert_eq!(result.changes_count, 10);
1276        assert!(result.conflicts.is_empty());
1277        assert!(result.error.is_none());
1278    }
1279
1280    #[test]
1281    fn test_sync_result_with_conflicts() {
1282        let conflict = SyncConflict {
1283            entity_id: EntityId::new(),
1284            entity_type: "request".to_string(),
1285            local_version: json!({"id": "local"}),
1286            remote_version: json!({"id": "remote"}),
1287            resolution: ConflictResolution::Manual,
1288        };
1289
1290        let result = SyncResult {
1291            success: false,
1292            changes_count: 0,
1293            conflicts: vec![conflict],
1294            error: Some("Conflicts detected".to_string()),
1295        };
1296
1297        assert!(!result.success);
1298        assert_eq!(result.conflicts.len(), 1);
1299        assert!(result.error.is_some());
1300    }
1301
1302    #[test]
1303    fn test_sync_conflict_creation() {
1304        let conflict = SyncConflict {
1305            entity_id: EntityId::new(),
1306            entity_type: "workspace".to_string(),
1307            local_version: json!({"name": "local"}),
1308            remote_version: json!({"name": "remote"}),
1309            resolution: ConflictResolution::Local,
1310        };
1311
1312        assert_eq!(conflict.entity_type, "workspace");
1313        match conflict.resolution {
1314            ConflictResolution::Local => {}
1315            _ => panic!(),
1316        }
1317    }
1318
1319    #[test]
1320    fn test_conflict_resolution_variants() {
1321        let local = ConflictResolution::Local;
1322        let remote = ConflictResolution::Remote;
1323        let manual = ConflictResolution::Manual;
1324
1325        match local {
1326            ConflictResolution::Local => {}
1327            _ => panic!(),
1328        }
1329        match remote {
1330            ConflictResolution::Remote => {}
1331            _ => panic!(),
1332        }
1333        match manual {
1334            ConflictResolution::Manual => {}
1335            _ => panic!(),
1336        }
1337    }
1338
1339    #[test]
1340    fn test_workspace_sync_manager_new() {
1341        let config = SyncConfig {
1342            enabled: true,
1343            provider: SyncProvider::Local {
1344                directory_path: "/tmp".to_string(),
1345                watch_changes: false,
1346            },
1347            interval_seconds: 30,
1348            conflict_strategy: ConflictResolutionStrategy::RemoteWins,
1349            auto_commit: false,
1350            auto_push: false,
1351            directory_structure: SyncDirectoryStructure::SingleDirectory,
1352            sync_direction: SyncDirection::LocalToRemote,
1353        };
1354
1355        let manager = WorkspaceSyncManager::new(config);
1356        assert!(manager.is_enabled());
1357        assert_eq!(manager.total_syncs, 0);
1358        assert_eq!(manager.successful_syncs, 0);
1359        assert_eq!(manager.failed_syncs, 0);
1360    }
1361
1362    #[test]
1363    fn test_workspace_sync_manager_default() {
1364        let manager = WorkspaceSyncManager::default();
1365        // Default config should have sync disabled
1366        assert!(!manager.is_enabled());
1367    }
1368
1369    #[test]
1370    fn test_workspace_sync_manager_get_config() {
1371        let config = SyncConfig {
1372            enabled: false,
1373            provider: SyncProvider::Local {
1374                directory_path: "/tmp".to_string(),
1375                watch_changes: false,
1376            },
1377            interval_seconds: 60,
1378            conflict_strategy: ConflictResolutionStrategy::Manual,
1379            auto_commit: true,
1380            auto_push: true,
1381            directory_structure: SyncDirectoryStructure::Hierarchical,
1382            sync_direction: SyncDirection::Bidirectional,
1383        };
1384
1385        let manager = WorkspaceSyncManager::new(config);
1386        let retrieved_config = manager.get_config();
1387        assert!(!retrieved_config.enabled);
1388        assert_eq!(retrieved_config.interval_seconds, 60);
1389    }
1390
1391    #[test]
1392    fn test_workspace_sync_manager_update_config() {
1393        let config1 = SyncConfig {
1394            enabled: false,
1395            provider: SyncProvider::Local {
1396                directory_path: "/tmp1".to_string(),
1397                watch_changes: false,
1398            },
1399            interval_seconds: 30,
1400            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1401            auto_commit: false,
1402            auto_push: false,
1403            directory_structure: SyncDirectoryStructure::SingleDirectory,
1404            sync_direction: SyncDirection::LocalToRemote,
1405        };
1406
1407        let mut manager = WorkspaceSyncManager::new(config1);
1408        assert!(!manager.is_enabled());
1409
1410        let config2 = SyncConfig {
1411            enabled: true,
1412            provider: SyncProvider::Local {
1413                directory_path: "/tmp2".to_string(),
1414                watch_changes: true,
1415            },
1416            interval_seconds: 120,
1417            conflict_strategy: ConflictResolutionStrategy::RemoteWins,
1418            auto_commit: true,
1419            auto_push: true,
1420            directory_structure: SyncDirectoryStructure::PerWorkspace,
1421            sync_direction: SyncDirection::Bidirectional,
1422        };
1423
1424        manager.update_config(config2);
1425        assert!(manager.is_enabled());
1426        assert_eq!(manager.get_config().interval_seconds, 120);
1427    }
1428
1429    #[test]
1430    fn test_workspace_sync_manager_get_status() {
1431        let config = SyncConfig {
1432            enabled: true,
1433            provider: SyncProvider::Local {
1434                directory_path: "/tmp".to_string(),
1435                watch_changes: false,
1436            },
1437            interval_seconds: 60,
1438            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1439            auto_commit: false,
1440            auto_push: false,
1441            directory_structure: SyncDirectoryStructure::SingleDirectory,
1442            sync_direction: SyncDirection::Bidirectional,
1443        };
1444
1445        let manager = WorkspaceSyncManager::new(config);
1446        let status = manager.get_status();
1447        assert_eq!(status.pending_changes, 0);
1448        assert_eq!(status.conflicts, 0);
1449        match status.state {
1450            SyncState::NotSynced => {}
1451            _ => panic!(),
1452        }
1453    }
1454
1455    #[test]
1456    fn test_workspace_sync_manager_get_conflicts() {
1457        let config = SyncConfig {
1458            enabled: true,
1459            provider: SyncProvider::Local {
1460                directory_path: "/tmp".to_string(),
1461                watch_changes: false,
1462            },
1463            interval_seconds: 60,
1464            conflict_strategy: ConflictResolutionStrategy::Manual,
1465            auto_commit: false,
1466            auto_push: false,
1467            directory_structure: SyncDirectoryStructure::SingleDirectory,
1468            sync_direction: SyncDirection::Bidirectional,
1469        };
1470
1471        let manager = WorkspaceSyncManager::new(config);
1472        let conflicts = manager.get_conflicts();
1473        assert!(conflicts.is_empty());
1474    }
1475
1476    #[test]
1477    fn test_workspace_sync_manager_is_enabled() {
1478        let config_enabled = SyncConfig {
1479            enabled: true,
1480            provider: SyncProvider::Local {
1481                directory_path: "/tmp".to_string(),
1482                watch_changes: false,
1483            },
1484            interval_seconds: 60,
1485            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1486            auto_commit: false,
1487            auto_push: false,
1488            directory_structure: SyncDirectoryStructure::SingleDirectory,
1489            sync_direction: SyncDirection::Bidirectional,
1490        };
1491
1492        let manager_enabled = WorkspaceSyncManager::new(config_enabled);
1493        assert!(manager_enabled.is_enabled());
1494
1495        let config_disabled = SyncConfig {
1496            enabled: false,
1497            provider: SyncProvider::Local {
1498                directory_path: "/tmp".to_string(),
1499                watch_changes: false,
1500            },
1501            interval_seconds: 60,
1502            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1503            auto_commit: false,
1504            auto_push: false,
1505            directory_structure: SyncDirectoryStructure::SingleDirectory,
1506            sync_direction: SyncDirection::Bidirectional,
1507        };
1508
1509        let manager_disabled = WorkspaceSyncManager::new(config_disabled);
1510        assert!(!manager_disabled.is_enabled());
1511    }
1512
1513    #[tokio::test]
1514    async fn test_sync_workspace_disabled() {
1515        // Test sync_workspace when disabled (lines 264-266)
1516        let config = SyncConfig {
1517            enabled: false,
1518            provider: SyncProvider::Local {
1519                directory_path: "/tmp/test".to_string(),
1520                watch_changes: false,
1521            },
1522            interval_seconds: 60,
1523            conflict_strategy: ConflictResolutionStrategy::Manual,
1524            auto_commit: false,
1525            auto_push: false,
1526            directory_structure: SyncDirectoryStructure::PerWorkspace,
1527            sync_direction: SyncDirection::Bidirectional,
1528        };
1529        let mut manager = WorkspaceSyncManager::new(config);
1530        let mut workspace = Workspace::new("Test Workspace".to_string());
1531
1532        // Should return error when disabled
1533        let result = manager.sync_workspace(&mut workspace).await;
1534        assert!(result.is_err());
1535        assert!(result.unwrap_err().contains("disabled"));
1536    }
1537
1538    #[tokio::test]
1539    async fn test_sync_workspace_local_to_remote() {
1540        // Test sync_workspace with Local provider LocalToRemote (lines 260-321, 778-811)
1541        let temp_dir = tempfile::tempdir().unwrap();
1542        let config = SyncConfig {
1543            enabled: true,
1544            provider: SyncProvider::Local {
1545                directory_path: temp_dir.path().to_string_lossy().to_string(),
1546                watch_changes: false,
1547            },
1548            interval_seconds: 60,
1549            conflict_strategy: ConflictResolutionStrategy::Manual,
1550            auto_commit: false,
1551            auto_push: false,
1552            directory_structure: SyncDirectoryStructure::PerWorkspace,
1553            sync_direction: SyncDirection::LocalToRemote,
1554        };
1555        let mut manager = WorkspaceSyncManager::new(config);
1556        let mut workspace = Workspace::new("Test Workspace".to_string());
1557
1558        // Should sync successfully (lines 260-321, 778-811)
1559        let result = manager.sync_workspace(&mut workspace).await;
1560        assert!(result.is_ok());
1561        let sync_result = result.unwrap();
1562        assert!(sync_result.success);
1563        assert_eq!(sync_result.changes_count, 1);
1564        assert!(matches!(manager.status.state, SyncState::Synced));
1565        assert_eq!(manager.total_syncs, 1);
1566        assert_eq!(manager.successful_syncs, 1);
1567    }
1568
1569    #[tokio::test]
1570    async fn test_sync_workspace_remote_to_local() {
1571        // Test sync_workspace with Local provider RemoteToLocal (lines 812-881)
1572        let temp_dir = tempfile::tempdir().unwrap();
1573        let config = SyncConfig {
1574            enabled: true,
1575            provider: SyncProvider::Local {
1576                directory_path: temp_dir.path().to_string_lossy().to_string(),
1577                watch_changes: false,
1578            },
1579            interval_seconds: 60,
1580            conflict_strategy: ConflictResolutionStrategy::Manual,
1581            auto_commit: false,
1582            auto_push: false,
1583            directory_structure: SyncDirectoryStructure::PerWorkspace,
1584            sync_direction: SyncDirection::RemoteToLocal,
1585        };
1586        let mut manager = WorkspaceSyncManager::new(config);
1587        let mut workspace = Workspace::new("Test Workspace".to_string());
1588
1589        // First write a workspace file to simulate remote
1590        let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1591        let remote_workspace = Workspace::new("Remote Workspace".to_string());
1592        let content = serde_yaml::to_string(&remote_workspace).unwrap();
1593        tokio::fs::write(&file_path, content).await.unwrap();
1594
1595        // Should sync successfully (lines 812-881)
1596        let result = manager.sync_workspace(&mut workspace).await;
1597        assert!(result.is_ok());
1598        let sync_result = result.unwrap();
1599        assert!(sync_result.success);
1600        assert_eq!(workspace.name, "Remote Workspace");
1601    }
1602
1603    #[tokio::test]
1604    async fn test_sync_workspace_remote_to_local_file_not_found() {
1605        // Test sync_workspace RemoteToLocal when file doesn't exist (lines 816-818)
1606        let temp_dir = tempfile::tempdir().unwrap();
1607        let config = SyncConfig {
1608            enabled: true,
1609            provider: SyncProvider::Local {
1610                directory_path: temp_dir.path().to_string_lossy().to_string(),
1611                watch_changes: false,
1612            },
1613            interval_seconds: 60,
1614            conflict_strategy: ConflictResolutionStrategy::Manual,
1615            auto_commit: false,
1616            auto_push: false,
1617            directory_structure: SyncDirectoryStructure::PerWorkspace,
1618            sync_direction: SyncDirection::RemoteToLocal,
1619        };
1620        let mut manager = WorkspaceSyncManager::new(config);
1621        let mut workspace = Workspace::new("Test Workspace".to_string());
1622
1623        // Should return error when file doesn't exist (lines 816-818)
1624        let result = manager.sync_workspace(&mut workspace).await;
1625        assert!(result.is_err());
1626        assert!(result.unwrap_err().contains("not found"));
1627    }
1628
1629    #[tokio::test]
1630    async fn test_sync_workspace_bidirectional() {
1631        // Test sync_workspace with Local provider Bidirectional (lines 882-925)
1632        let temp_dir = tempfile::tempdir().unwrap();
1633        let config = SyncConfig {
1634            enabled: true,
1635            provider: SyncProvider::Local {
1636                directory_path: temp_dir.path().to_string_lossy().to_string(),
1637                watch_changes: false,
1638            },
1639            interval_seconds: 60,
1640            conflict_strategy: ConflictResolutionStrategy::Manual,
1641            auto_commit: false,
1642            auto_push: false,
1643            directory_structure: SyncDirectoryStructure::PerWorkspace,
1644            sync_direction: SyncDirection::Bidirectional,
1645        };
1646        let mut manager = WorkspaceSyncManager::new(config);
1647        let mut workspace = Workspace::new("Test Workspace".to_string());
1648
1649        // Should sync successfully (lines 882-925)
1650        let result = manager.sync_workspace(&mut workspace).await;
1651        assert!(result.is_ok());
1652        let sync_result = result.unwrap();
1653        assert!(sync_result.success);
1654        assert_eq!(sync_result.changes_count, 1);
1655    }
1656
1657    #[tokio::test]
1658    async fn test_sync_workspace_bidirectional_with_conflicts() {
1659        // Test sync_workspace Bidirectional with conflicts (lines 897-908)
1660        let temp_dir = tempfile::tempdir().unwrap();
1661        let config = SyncConfig {
1662            enabled: true,
1663            provider: SyncProvider::Local {
1664                directory_path: temp_dir.path().to_string_lossy().to_string(),
1665                watch_changes: false,
1666            },
1667            interval_seconds: 60,
1668            conflict_strategy: ConflictResolutionStrategy::Manual,
1669            auto_commit: false,
1670            auto_push: false,
1671            directory_structure: SyncDirectoryStructure::PerWorkspace,
1672            sync_direction: SyncDirection::Bidirectional,
1673        };
1674        let mut manager = WorkspaceSyncManager::new(config);
1675        let mut workspace = Workspace::new("Test Workspace".to_string());
1676
1677        // Create remote workspace with newer timestamp
1678        // First set local timestamp
1679        workspace.updated_at = chrono::Utc::now();
1680        std::thread::sleep(std::time::Duration::from_millis(10));
1681        // Then create remote with newer timestamp
1682        let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
1683        remote_workspace.updated_at = chrono::Utc::now(); // Remote is newer
1684
1685        let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1686        let content = serde_yaml::to_string(&remote_workspace).unwrap();
1687        tokio::fs::write(&file_path, content).await.unwrap();
1688
1689        // Should detect conflicts (lines 897-908)
1690        let result = manager.sync_workspace(&mut workspace).await;
1691        assert!(result.is_ok());
1692        let sync_result = result.unwrap();
1693        assert!(sync_result.success);
1694        assert!(!sync_result.conflicts.is_empty());
1695    }
1696
1697    #[tokio::test]
1698    async fn test_sync_workspace_remote_to_local_with_conflicts() {
1699        // Test sync_workspace RemoteToLocal with conflicts (lines 832-862)
1700        let temp_dir = tempfile::tempdir().unwrap();
1701        let config = SyncConfig {
1702            enabled: true,
1703            provider: SyncProvider::Local {
1704                directory_path: temp_dir.path().to_string_lossy().to_string(),
1705                watch_changes: false,
1706            },
1707            interval_seconds: 60,
1708            conflict_strategy: ConflictResolutionStrategy::Manual,
1709            auto_commit: false,
1710            auto_push: false,
1711            directory_structure: SyncDirectoryStructure::PerWorkspace,
1712            sync_direction: SyncDirection::RemoteToLocal,
1713        };
1714        let mut manager = WorkspaceSyncManager::new(config);
1715        let mut workspace = Workspace::new("Test Workspace".to_string());
1716        // Create remote workspace with older timestamp first
1717        let mut remote_workspace = Workspace::new("Remote Workspace".to_string());
1718        remote_workspace.updated_at = chrono::Utc::now();
1719        std::thread::sleep(std::time::Duration::from_millis(10));
1720        workspace.updated_at = chrono::Utc::now(); // Local is newer
1721
1722        let file_path = temp_dir.path().join(format!("{}.yaml", workspace.id));
1723        let content = serde_yaml::to_string(&remote_workspace).unwrap();
1724        tokio::fs::write(&file_path, content).await.unwrap();
1725
1726        // Should detect conflicts (lines 832-862)
1727        let result = manager.sync_workspace(&mut workspace).await;
1728        assert!(result.is_ok());
1729        let sync_result = result.unwrap();
1730        assert!(sync_result.success);
1731        assert!(!sync_result.conflicts.is_empty());
1732    }
1733
1734    #[tokio::test]
1735    async fn test_sync_workspace_success_tracking() {
1736        // Test sync_workspace success tracking (lines 299-311)
1737        let temp_dir = tempfile::tempdir().unwrap();
1738        let config = SyncConfig {
1739            enabled: true,
1740            provider: SyncProvider::Local {
1741                directory_path: temp_dir.path().to_string_lossy().to_string(),
1742                watch_changes: false,
1743            },
1744            interval_seconds: 60,
1745            conflict_strategy: ConflictResolutionStrategy::Manual,
1746            auto_commit: false,
1747            auto_push: false,
1748            directory_structure: SyncDirectoryStructure::PerWorkspace,
1749            sync_direction: SyncDirection::LocalToRemote,
1750        };
1751        let mut manager = WorkspaceSyncManager::new(config);
1752        let mut workspace = Workspace::new("Test Workspace".to_string());
1753
1754        // Should track successful sync (lines 299-311)
1755        let result = manager.sync_workspace(&mut workspace).await;
1756        assert!(result.is_ok());
1757        assert!(matches!(manager.status.state, SyncState::Synced));
1758        assert_eq!(manager.successful_syncs, 1);
1759        assert_eq!(manager.total_syncs, 1);
1760        assert!(manager.status.last_sync.is_some());
1761        assert_eq!(manager.status.pending_changes, 0);
1762    }
1763
1764    #[tokio::test]
1765    async fn test_sync_workspace_error_tracking() {
1766        // Test sync_workspace error tracking (lines 313-317)
1767        let config = SyncConfig {
1768            enabled: true,
1769            provider: SyncProvider::Local {
1770                directory_path: "/nonexistent/path/that/does/not/exist".to_string(),
1771                watch_changes: false,
1772            },
1773            interval_seconds: 60,
1774            conflict_strategy: ConflictResolutionStrategy::Manual,
1775            auto_commit: false,
1776            auto_push: false,
1777            directory_structure: SyncDirectoryStructure::PerWorkspace,
1778            sync_direction: SyncDirection::RemoteToLocal,
1779        };
1780        let mut manager = WorkspaceSyncManager::new(config);
1781        let mut workspace = Workspace::new("Test Workspace".to_string());
1782
1783        // Should track failed sync (lines 313-317)
1784        let result = manager.sync_workspace(&mut workspace).await;
1785        // May succeed or fail depending on directory creation, but should track state
1786        if result.is_err() {
1787            assert!(matches!(manager.status.state, SyncState::SyncFailed));
1788            assert_eq!(manager.failed_syncs, 1);
1789            assert!(manager.status.last_error.is_some());
1790        }
1791    }
1792
1793    #[tokio::test]
1794    async fn test_sync_workspace_duration_tracking() {
1795        // Test sync_workspace duration tracking (lines 294-297)
1796        let temp_dir = tempfile::tempdir().unwrap();
1797        let config = SyncConfig {
1798            enabled: true,
1799            provider: SyncProvider::Local {
1800                directory_path: temp_dir.path().to_string_lossy().to_string(),
1801                watch_changes: false,
1802            },
1803            interval_seconds: 60,
1804            conflict_strategy: ConflictResolutionStrategy::Manual,
1805            auto_commit: false,
1806            auto_push: false,
1807            directory_structure: SyncDirectoryStructure::PerWorkspace,
1808            sync_direction: SyncDirection::LocalToRemote,
1809        };
1810        let mut manager = WorkspaceSyncManager::new(config);
1811        let mut workspace = Workspace::new("Test Workspace".to_string());
1812
1813        // Should track sync duration (lines 294-297)
1814        let result = manager.sync_workspace(&mut workspace).await;
1815        assert!(result.is_ok());
1816        assert!(manager.last_sync_duration_ms.is_some());
1817        let _ = manager.last_sync_duration_ms.unwrap(); // Duration is u64, always >= 0
1818    }
1819
1820    #[tokio::test]
1821    async fn test_sync_workspace_state_transitions() {
1822        // Test sync_workspace state transitions (lines 274-275, 303-310)
1823        let temp_dir = tempfile::tempdir().unwrap();
1824        let config = SyncConfig {
1825            enabled: true,
1826            provider: SyncProvider::Local {
1827                directory_path: temp_dir.path().to_string_lossy().to_string(),
1828                watch_changes: false,
1829            },
1830            interval_seconds: 60,
1831            conflict_strategy: ConflictResolutionStrategy::Manual,
1832            auto_commit: false,
1833            auto_push: false,
1834            directory_structure: SyncDirectoryStructure::PerWorkspace,
1835            sync_direction: SyncDirection::LocalToRemote,
1836        };
1837        let mut manager = WorkspaceSyncManager::new(config);
1838        let mut workspace = Workspace::new("Test Workspace".to_string());
1839
1840        // Should transition to Syncing then Synced (lines 274-275, 303-310)
1841        assert!(matches!(manager.status.state, SyncState::NotSynced));
1842        let result = manager.sync_workspace(&mut workspace).await;
1843        assert!(result.is_ok());
1844        assert!(matches!(manager.status.state, SyncState::Synced));
1845    }
1846}