mockforge_collab/
merge.rs

1//! Workspace merge operations and conflict resolution
2
3use crate::error::{CollabError, Result};
4use crate::history::VersionControl;
5use crate::models::{ConflictType, MergeConflict, MergeStatus, WorkspaceMerge};
6use chrono::Utc;
7use serde_json::Value;
8use sqlx::{Pool, Sqlite};
9use uuid::Uuid;
10
11/// Merge service for handling workspace merges
12pub struct MergeService {
13    db: Pool<Sqlite>,
14    version_control: VersionControl,
15}
16
17impl MergeService {
18    /// Create a new merge service
19    #[must_use]
20    pub fn new(db: Pool<Sqlite>) -> Self {
21        Self {
22            db: db.clone(),
23            version_control: VersionControl::new(db),
24        }
25    }
26
27    /// Find the common ancestor commit between two workspaces
28    ///
29    /// This uses a simple approach: find the fork point if one exists,
30    /// otherwise find the earliest common commit in both histories.
31    pub async fn find_common_ancestor(
32        &self,
33        source_workspace_id: Uuid,
34        target_workspace_id: Uuid,
35    ) -> Result<Option<Uuid>> {
36        // First, check if target is a fork of source
37        let source_ws_id_str = source_workspace_id.to_string();
38        let target_ws_id_str = target_workspace_id.to_string();
39        let fork = sqlx::query!(
40            r#"
41            SELECT fork_point_commit_id
42            FROM workspace_forks
43            WHERE source_workspace_id = ? AND forked_workspace_id = ?
44            "#,
45            source_ws_id_str,
46            target_ws_id_str
47        )
48        .fetch_optional(&self.db)
49        .await?;
50
51        if let Some(fork) = fork {
52            if let Some(commit_id_str) = fork.fork_point_commit_id.as_ref() {
53                if let Ok(commit_id) = Uuid::parse_str(commit_id_str) {
54                    return Ok(Some(commit_id));
55                }
56            }
57        }
58
59        // Check if source is a fork of target
60        let target_ws_id_str2 = target_workspace_id.to_string();
61        let source_ws_id_str2 = source_workspace_id.to_string();
62        let fork = sqlx::query!(
63            r#"
64            SELECT fork_point_commit_id
65            FROM workspace_forks
66            WHERE source_workspace_id = ? AND forked_workspace_id = ?
67            "#,
68            target_ws_id_str2,
69            source_ws_id_str2
70        )
71        .fetch_optional(&self.db)
72        .await?;
73
74        if let Some(fork) = fork {
75            if let Some(commit_id_str) = fork.fork_point_commit_id.as_ref() {
76                if let Ok(commit_id) = Uuid::parse_str(commit_id_str) {
77                    return Ok(Some(commit_id));
78                }
79            }
80        }
81
82        // Implement sophisticated common ancestor finding by walking commit history
83        // This finds the Lowest Common Ancestor (LCA) by walking both commit histories
84        let source_commits =
85            self.version_control.get_history(source_workspace_id, Some(1000)).await?;
86        let target_commits =
87            self.version_control.get_history(target_workspace_id, Some(1000)).await?;
88
89        // Build commit ID sets for fast lookup
90        let source_commit_ids: std::collections::HashSet<Uuid> =
91            source_commits.iter().map(|c| c.id).collect();
92        let target_commit_ids: std::collections::HashSet<Uuid> =
93            target_commits.iter().map(|c| c.id).collect();
94
95        // Find the first commit that appears in both histories (LCA)
96        // Walk from most recent to oldest in source history
97        for source_commit in &source_commits {
98            if target_commit_ids.contains(&source_commit.id) {
99                return Ok(Some(source_commit.id));
100            }
101        }
102
103        // If no direct match, try walking parent chains
104        // Get the latest commits
105        if let (Some(source_latest), Some(target_latest)) =
106            (source_commits.first(), target_commits.first())
107        {
108            // Build ancestor sets by walking parent chains
109            let source_ancestors = self.build_ancestor_set(source_latest.id).await?;
110            let target_ancestors = self.build_ancestor_set(target_latest.id).await?;
111
112            // Find the first common ancestor
113            for ancestor in &source_ancestors {
114                if target_ancestors.contains(ancestor) {
115                    return Ok(Some(*ancestor));
116                }
117            }
118        }
119
120        // No common ancestor found
121        Ok(None)
122    }
123
124    /// Perform a three-way merge between two workspaces
125    ///
126    /// Merges changes from `source_workspace` into `target_workspace`.
127    /// Returns the merged state and any conflicts.
128    pub async fn merge_workspaces(
129        &self,
130        source_workspace_id: Uuid,
131        target_workspace_id: Uuid,
132        user_id: Uuid,
133    ) -> Result<(Value, Vec<MergeConflict>)> {
134        // Get latest commits for both workspaces
135        let source_commit =
136            self.version_control.get_latest_commit(source_workspace_id).await?.ok_or_else(
137                || CollabError::Internal("Source workspace has no commits".to_string()),
138            )?;
139
140        let target_commit =
141            self.version_control.get_latest_commit(target_workspace_id).await?.ok_or_else(
142                || CollabError::Internal("Target workspace has no commits".to_string()),
143            )?;
144
145        // Find common ancestor
146        let base_commit_id = self
147            .find_common_ancestor(source_workspace_id, target_workspace_id)
148            .await?
149            .ok_or_else(|| {
150                CollabError::Internal(
151                    "Cannot find common ancestor. Workspaces must be related by fork.".to_string(),
152                )
153            })?;
154
155        let base_commit = self.version_control.get_commit(base_commit_id).await?;
156
157        // Perform three-way merge
158        let (merged_state, conflicts) = self.three_way_merge(
159            &base_commit.snapshot,
160            &source_commit.snapshot,
161            &target_commit.snapshot,
162        )?;
163
164        // Create merge record
165        let mut merge = WorkspaceMerge::new(
166            source_workspace_id,
167            target_workspace_id,
168            base_commit_id,
169            source_commit.id,
170            target_commit.id,
171        );
172
173        if conflicts.is_empty() {
174            merge.status = MergeStatus::Completed;
175        } else {
176            merge.status = MergeStatus::Conflict;
177            merge.conflict_data = Some(serde_json::to_value(&conflicts)?);
178        }
179
180        // Save merge record
181        let merge_id_str = merge.id.to_string();
182        let source_ws_id_str = merge.source_workspace_id.to_string();
183        let target_ws_id_str = merge.target_workspace_id.to_string();
184        let base_commit_id_str = merge.base_commit_id.to_string();
185        let source_commit_id_str = merge.source_commit_id.to_string();
186        let target_commit_id_str = merge.target_commit_id.to_string();
187        let merge_commit_id_str = merge.merge_commit_id.map(|id| id.to_string());
188        let status_str = serde_json::to_string(&merge.status)?;
189        let conflict_data_str =
190            merge.conflict_data.as_ref().map(serde_json::to_string).transpose()?;
191        let merged_by_str = merge.merged_by.map(|id| id.to_string());
192        let merged_at_str = merge.merged_at.map(|dt| dt.to_rfc3339());
193        let created_at_str = merge.created_at.to_rfc3339();
194
195        sqlx::query!(
196            r#"
197            INSERT INTO workspace_merges (
198                id, source_workspace_id, target_workspace_id,
199                base_commit_id, source_commit_id, target_commit_id,
200                merge_commit_id, status, conflict_data, merged_by, merged_at, created_at
201            )
202            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
203            "#,
204            merge_id_str,
205            source_ws_id_str,
206            target_ws_id_str,
207            base_commit_id_str,
208            source_commit_id_str,
209            target_commit_id_str,
210            merge_commit_id_str,
211            status_str,
212            conflict_data_str,
213            merged_by_str,
214            merged_at_str,
215            created_at_str
216        )
217        .execute(&self.db)
218        .await?;
219
220        Ok((merged_state, conflicts))
221    }
222
223    /// Perform a three-way merge on JSON values
224    ///
225    /// Implements a simple three-way merge algorithm:
226    /// - If base == source, use target
227    /// - If base == target, use source
228    /// - If source == target, use either
229    /// - Otherwise, conflict
230    fn three_way_merge(
231        &self,
232        base: &Value,
233        source: &Value,
234        target: &Value,
235    ) -> Result<(Value, Vec<MergeConflict>)> {
236        let mut merged = target.clone();
237        let mut conflicts = Vec::new();
238
239        self.merge_value("", base, source, target, &mut merged, &mut conflicts)?;
240
241        Ok((merged, conflicts))
242    }
243
244    /// Recursively merge JSON values
245    fn merge_value(
246        &self,
247        path: &str,
248        base: &Value,
249        source: &Value,
250        target: &Value,
251        merged: &mut Value,
252        conflicts: &mut Vec<MergeConflict>,
253    ) -> Result<()> {
254        match (base, source, target) {
255            // No changes: base == source == target
256            (b, s, t) if b == s && s == t => {
257                // Already correct, do nothing
258            }
259
260            // Only target changed: base == source, target differs
261            (b, s, t) if b == s && t != b => {
262                // Target is already in merged, keep it
263            }
264
265            // Only source changed: base == target, source differs
266            (b, s, t) if b == t && s != b => {
267                *merged = source.clone();
268            }
269
270            // Both changed the same way: source == target, both differ from base
271            (b, s, t) if s == t && s != b => {
272                *merged = source.clone();
273            }
274
275            // Handle objects recursively - MUST come before generic conflict handler
276            (Value::Object(base_obj), Value::Object(source_obj), Value::Object(target_obj)) => {
277                if let Value::Object(merged_obj) = merged {
278                    // Get all keys from all three objects
279                    let all_keys: std::collections::HashSet<_> =
280                        base_obj.keys().chain(source_obj.keys()).chain(target_obj.keys()).collect();
281
282                    for key in all_keys {
283                        let base_val = base_obj.get(key);
284                        let source_val = source_obj.get(key);
285                        let target_val = target_obj.get(key);
286
287                        let new_path = if path.is_empty() {
288                            key.clone()
289                        } else {
290                            format!("{path}.{key}")
291                        };
292
293                        match (base_val, source_val, target_val) {
294                            // Key only in source
295                            (None, Some(s), None) => {
296                                merged_obj.insert(key.clone(), s.clone());
297                            }
298                            // Key only in target
299                            (None, None, Some(t)) => {
300                                merged_obj.insert(key.clone(), t.clone());
301                            }
302                            // Key in both source and target (but not base) - conflict
303                            (None, Some(s), Some(t)) if s != t => {
304                                conflicts.push(MergeConflict {
305                                    path: new_path.clone(),
306                                    base_value: None,
307                                    source_value: Some(s.clone()),
308                                    target_value: Some(t.clone()),
309                                    conflict_type: ConflictType::BothAdded,
310                                });
311                                // Keep target value
312                            }
313                            // Key in both, same value
314                            (None, Some(s), Some(t)) if s == t => {
315                                merged_obj.insert(key.clone(), s.clone());
316                            }
317                            // Key exists in all three - recurse
318                            (Some(b), Some(s), Some(t)) => {
319                                if let Some(merged_val) = merged_obj.get_mut(key) {
320                                    self.merge_value(&new_path, b, s, t, merged_val, conflicts)?;
321                                }
322                            }
323                            // Key deleted in source
324                            (Some(b), None, Some(t)) if b == t => {
325                                merged_obj.remove(key);
326                            }
327                            // Key deleted in target
328                            (Some(b), Some(s), None) if b == s => {
329                                merged_obj.remove(key);
330                            }
331                            // Key deleted in source, modified in target - conflict
332                            (Some(b), None, Some(_t)) => {
333                                conflicts.push(MergeConflict {
334                                    path: new_path.clone(),
335                                    base_value: Some(b.clone()),
336                                    source_value: source_val.cloned(),
337                                    target_value: target_val.cloned(),
338                                    conflict_type: ConflictType::DeletedModified,
339                                });
340                            }
341                            // Key deleted in target, modified in source - conflict
342                            (Some(b), Some(_s), None) => {
343                                conflicts.push(MergeConflict {
344                                    path: new_path.clone(),
345                                    base_value: Some(b.clone()),
346                                    source_value: source_val.cloned(),
347                                    target_value: target_val.cloned(),
348                                    conflict_type: ConflictType::DeletedModified,
349                                });
350                            }
351                            _ => {}
352                        }
353                    }
354                }
355            }
356
357            // Handle arrays - simple approach: use target, mark as conflict if different
358            (Value::Array(base_arr), Value::Array(source_arr), Value::Array(target_arr)) => {
359                if (base_arr != source_arr || base_arr != target_arr) && source_arr != target_arr {
360                    conflicts.push(MergeConflict {
361                        path: path.to_string(),
362                        base_value: Some(base.clone()),
363                        source_value: Some(source.clone()),
364                        target_value: Some(target.clone()),
365                        conflict_type: ConflictType::Modified,
366                    });
367                }
368            }
369
370            // Conflict: both changed differently (catch-all for non-Object/Array types)
371            (b, s, t) if s != t && s != b && t != b => {
372                conflicts.push(MergeConflict {
373                    path: path.to_string(),
374                    base_value: Some(b.clone()),
375                    source_value: Some(s.clone()),
376                    target_value: Some(t.clone()),
377                    conflict_type: ConflictType::Modified,
378                });
379                // Keep target value for now (user can resolve later)
380            }
381
382            _ => {
383                // For other types that don't match above patterns
384            }
385        }
386
387        Ok(())
388    }
389
390    /// Complete a merge by creating a merge commit
391    pub async fn complete_merge(
392        &self,
393        merge_id: Uuid,
394        user_id: Uuid,
395        resolved_state: Value,
396        message: String,
397    ) -> Result<Uuid> {
398        // Get merge record
399        let merge = self.get_merge(merge_id).await?;
400
401        if merge.status != MergeStatus::Conflict && merge.status != MergeStatus::Pending {
402            return Err(CollabError::InvalidInput(
403                "Merge is not in a state that can be completed".to_string(),
404            ));
405        }
406
407        // Create merge commit
408        let merge_commit = self
409            .version_control
410            .create_commit(
411                merge.target_workspace_id,
412                user_id,
413                message,
414                Some(merge.target_commit_id),
415                // Version will be incremented by workspace service
416                0, // Placeholder, will be set properly
417                resolved_state.clone(),
418                serde_json::json!({
419                    "type": "merge",
420                    "source_workspace_id": merge.source_workspace_id,
421                    "source_commit_id": merge.source_commit_id,
422                }),
423            )
424            .await?;
425
426        // Update merge record
427        let now = Utc::now();
428        sqlx::query!(
429            r#"
430            UPDATE workspace_merges
431            SET merge_commit_id = ?, status = ?, merged_by = ?, merged_at = ?
432            WHERE id = ?
433            "#,
434            merge_commit.id,
435            MergeStatus::Completed,
436            user_id,
437            now,
438            merge_id
439        )
440        .execute(&self.db)
441        .await?;
442
443        Ok(merge_commit.id)
444    }
445
446    /// Get a merge by ID
447    pub async fn get_merge(&self, merge_id: Uuid) -> Result<WorkspaceMerge> {
448        let merge_id_str = merge_id.to_string();
449        let row = sqlx::query!(
450            r#"
451            SELECT
452                id,
453                source_workspace_id,
454                target_workspace_id,
455                base_commit_id,
456                source_commit_id,
457                target_commit_id,
458                merge_commit_id,
459                status,
460                conflict_data,
461                merged_by,
462                merged_at,
463                created_at
464            FROM workspace_merges
465            WHERE id = ?
466            "#,
467            merge_id_str
468        )
469        .fetch_optional(&self.db)
470        .await?
471        .ok_or_else(|| CollabError::Internal(format!("Merge not found: {merge_id}")))?;
472
473        Ok(WorkspaceMerge {
474            id: Uuid::parse_str(&row.id)
475                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
476            source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
477                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
478            target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
479                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
480            base_commit_id: Uuid::parse_str(&row.base_commit_id)
481                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
482            source_commit_id: Uuid::parse_str(&row.source_commit_id)
483                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
484            target_commit_id: Uuid::parse_str(&row.target_commit_id)
485                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
486            merge_commit_id: row.merge_commit_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
487            status: serde_json::from_str(&row.status)
488                .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
489            conflict_data: row.conflict_data.as_ref().and_then(|s| serde_json::from_str(s).ok()),
490            merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
491            merged_at: row
492                .merged_at
493                .as_ref()
494                .map(|s| {
495                    chrono::DateTime::parse_from_rfc3339(s)
496                        .map(|dt| dt.with_timezone(&Utc))
497                        .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))
498                })
499                .transpose()?,
500            created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
501                .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
502                .with_timezone(&Utc),
503        })
504    }
505
506    /// List merges for a workspace
507    pub async fn list_merges(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMerge>> {
508        let workspace_id_str = workspace_id.to_string();
509        let rows = sqlx::query!(
510            r#"
511            SELECT
512                id,
513                source_workspace_id,
514                target_workspace_id,
515                base_commit_id,
516                source_commit_id,
517                target_commit_id,
518                merge_commit_id,
519                status,
520                conflict_data,
521                merged_by,
522                merged_at,
523                created_at
524            FROM workspace_merges
525            WHERE source_workspace_id = ? OR target_workspace_id = ?
526            ORDER BY created_at DESC
527            "#,
528            workspace_id_str,
529            workspace_id_str
530        )
531        .fetch_all(&self.db)
532        .await?;
533
534        let merges: Result<Vec<WorkspaceMerge>> = rows
535            .into_iter()
536            .map(|row| {
537                Ok(WorkspaceMerge {
538                    id: Uuid::parse_str(&row.id)
539                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
540                    source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
541                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
542                    target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
543                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
544                    base_commit_id: Uuid::parse_str(&row.base_commit_id)
545                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
546                    source_commit_id: Uuid::parse_str(&row.source_commit_id)
547                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
548                    target_commit_id: Uuid::parse_str(&row.target_commit_id)
549                        .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
550                    merge_commit_id: row
551                        .merge_commit_id
552                        .as_ref()
553                        .and_then(|s| Uuid::parse_str(s).ok()),
554                    status: serde_json::from_str(&row.status)
555                        .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
556                    conflict_data: row
557                        .conflict_data
558                        .as_ref()
559                        .and_then(|s| serde_json::from_str(s).ok()),
560                    merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
561                    merged_at: row
562                        .merged_at
563                        .as_ref()
564                        .map(|s| {
565                            chrono::DateTime::parse_from_rfc3339(s)
566                                .map(|dt| dt.with_timezone(&Utc))
567                                .map_err(|e| {
568                                    CollabError::Internal(format!("Invalid timestamp: {e}"))
569                                })
570                        })
571                        .transpose()?,
572                    created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
573                        .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
574                        .with_timezone(&Utc),
575                })
576            })
577            .collect();
578        let merges = merges?;
579
580        Ok(merges)
581    }
582
583    /// Build a set of all ancestor commit IDs by walking the parent chain
584    async fn build_ancestor_set(&self, commit_id: Uuid) -> Result<std::collections::HashSet<Uuid>> {
585        let mut ancestors = std::collections::HashSet::new();
586        let mut current_id = Some(commit_id);
587        let mut visited = std::collections::HashSet::new();
588
589        // Walk the parent chain up to a reasonable depth (prevent infinite loops)
590        let max_depth = 1000;
591        let mut depth = 0;
592
593        while let Some(id) = current_id {
594            if visited.contains(&id) || depth > max_depth {
595                break; // Cycle detected or max depth reached
596            }
597            visited.insert(id);
598            ancestors.insert(id);
599
600            // Get the commit and move to parent
601            match self.version_control.get_commit(id).await {
602                Ok(commit) => {
603                    current_id = commit.parent_id;
604                    depth += 1;
605                }
606                Err(_) => break, // Commit not found, stop walking
607            }
608        }
609
610        Ok(ancestors)
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617    use serde_json::json;
618    use sqlx::SqlitePool;
619
620    async fn setup_test_db() -> Pool<Sqlite> {
621        let pool = SqlitePool::connect(":memory:").await.unwrap();
622
623        // Create workspace_forks table
624        sqlx::query(
625            r#"
626            CREATE TABLE IF NOT EXISTS workspace_forks (
627                id TEXT PRIMARY KEY,
628                source_workspace_id TEXT NOT NULL,
629                forked_workspace_id TEXT NOT NULL,
630                fork_point_commit_id TEXT,
631                created_at TEXT NOT NULL,
632                created_by TEXT NOT NULL
633            )
634            "#,
635        )
636        .execute(&pool)
637        .await
638        .unwrap();
639
640        // Create workspace_merges table
641        sqlx::query(
642            r#"
643            CREATE TABLE IF NOT EXISTS workspace_merges (
644                id TEXT PRIMARY KEY,
645                source_workspace_id TEXT NOT NULL,
646                target_workspace_id TEXT NOT NULL,
647                base_commit_id TEXT NOT NULL,
648                source_commit_id TEXT NOT NULL,
649                target_commit_id TEXT NOT NULL,
650                merge_commit_id TEXT,
651                status TEXT NOT NULL,
652                conflict_data TEXT,
653                merged_by TEXT,
654                merged_at TEXT,
655                created_at TEXT NOT NULL
656            )
657            "#,
658        )
659        .execute(&pool)
660        .await
661        .unwrap();
662
663        // Create commits table
664        sqlx::query(
665            r#"
666            CREATE TABLE IF NOT EXISTS commits (
667                id TEXT PRIMARY KEY,
668                workspace_id TEXT NOT NULL,
669                user_id TEXT NOT NULL,
670                message TEXT NOT NULL,
671                parent_id TEXT,
672                version INTEGER NOT NULL,
673                snapshot TEXT NOT NULL,
674                metadata TEXT,
675                created_at TEXT NOT NULL
676            )
677            "#,
678        )
679        .execute(&pool)
680        .await
681        .unwrap();
682
683        pool
684    }
685
686    #[tokio::test]
687    async fn test_merge_service_new() {
688        let pool = setup_test_db().await;
689        let service = MergeService::new(pool);
690
691        // Just verify service is created
692        // We can't test much without real commits
693        assert!(true);
694    }
695
696    #[test]
697    fn test_three_way_merge_no_changes() {
698        let pool_fut = setup_test_db();
699        let rt = tokio::runtime::Runtime::new().unwrap();
700        let pool = rt.block_on(pool_fut);
701        let service = MergeService::new(pool);
702
703        let base = json!({"key": "value"});
704        let source = json!({"key": "value"});
705        let target = json!({"key": "value"});
706
707        let result = service.three_way_merge(&base, &source, &target);
708        assert!(result.is_ok());
709
710        let (merged, conflicts) = result.unwrap();
711        assert_eq!(merged, target);
712        assert!(conflicts.is_empty());
713    }
714
715    #[test]
716    fn test_three_way_merge_source_change() {
717        let pool_fut = setup_test_db();
718        let rt = tokio::runtime::Runtime::new().unwrap();
719        let pool = rt.block_on(pool_fut);
720        let service = MergeService::new(pool);
721
722        let base = json!({"key": "value"});
723        let source = json!({"key": "new_value"});
724        let target = json!({"key": "value"});
725
726        let result = service.three_way_merge(&base, &source, &target);
727        assert!(result.is_ok());
728
729        let (merged, conflicts) = result.unwrap();
730        assert_eq!(merged, source);
731        assert!(conflicts.is_empty());
732    }
733
734    #[test]
735    fn test_three_way_merge_target_change() {
736        let pool_fut = setup_test_db();
737        let rt = tokio::runtime::Runtime::new().unwrap();
738        let pool = rt.block_on(pool_fut);
739        let service = MergeService::new(pool);
740
741        let base = json!({"key": "value"});
742        let source = json!({"key": "value"});
743        let target = json!({"key": "new_value"});
744
745        let result = service.three_way_merge(&base, &source, &target);
746        assert!(result.is_ok());
747
748        let (merged, conflicts) = result.unwrap();
749        assert_eq!(merged, target);
750        assert!(conflicts.is_empty());
751    }
752
753    #[test]
754    fn test_three_way_merge_both_changed_same() {
755        let pool_fut = setup_test_db();
756        let rt = tokio::runtime::Runtime::new().unwrap();
757        let pool = rt.block_on(pool_fut);
758        let service = MergeService::new(pool);
759
760        let base = json!({"key": "value"});
761        let source = json!({"key": "new_value"});
762        let target = json!({"key": "new_value"});
763
764        let result = service.three_way_merge(&base, &source, &target);
765        assert!(result.is_ok());
766
767        let (merged, conflicts) = result.unwrap();
768        assert_eq!(merged, source);
769        assert!(conflicts.is_empty());
770    }
771
772    #[test]
773    fn test_three_way_merge_conflict() {
774        let pool_fut = setup_test_db();
775        let rt = tokio::runtime::Runtime::new().unwrap();
776        let pool = rt.block_on(pool_fut);
777        let service = MergeService::new(pool);
778
779        let base = json!({"key": "value"});
780        let source = json!({"key": "source_value"});
781        let target = json!({"key": "target_value"});
782
783        let result = service.three_way_merge(&base, &source, &target);
784        assert!(result.is_ok());
785
786        let (merged, conflicts) = result.unwrap();
787        assert_eq!(merged, target); // Target is kept on conflict
788        assert_eq!(conflicts.len(), 1);
789        assert_eq!(conflicts[0].path, "key");
790        assert_eq!(conflicts[0].conflict_type, ConflictType::Modified);
791    }
792
793    #[test]
794    fn test_three_way_merge_object_add_source() {
795        let pool_fut = setup_test_db();
796        let rt = tokio::runtime::Runtime::new().unwrap();
797        let pool = rt.block_on(pool_fut);
798        let service = MergeService::new(pool);
799
800        let base = json!({});
801        let source = json!({"new_key": "value"});
802        let target = json!({});
803
804        let result = service.three_way_merge(&base, &source, &target);
805        assert!(result.is_ok());
806
807        let (merged, conflicts) = result.unwrap();
808        assert_eq!(merged.get("new_key"), Some(&json!("value")));
809        assert!(conflicts.is_empty());
810    }
811
812    #[test]
813    fn test_three_way_merge_object_add_target() {
814        let pool_fut = setup_test_db();
815        let rt = tokio::runtime::Runtime::new().unwrap();
816        let pool = rt.block_on(pool_fut);
817        let service = MergeService::new(pool);
818
819        let base = json!({});
820        let source = json!({});
821        let target = json!({"new_key": "value"});
822
823        let result = service.three_way_merge(&base, &source, &target);
824        assert!(result.is_ok());
825
826        let (merged, conflicts) = result.unwrap();
827        assert_eq!(merged.get("new_key"), Some(&json!("value")));
828        assert!(conflicts.is_empty());
829    }
830
831    #[test]
832    fn test_three_way_merge_both_added_different() {
833        let pool_fut = setup_test_db();
834        let rt = tokio::runtime::Runtime::new().unwrap();
835        let pool = rt.block_on(pool_fut);
836        let service = MergeService::new(pool);
837
838        let base = json!({});
839        let source = json!({"key": "source_value"});
840        let target = json!({"key": "target_value"});
841
842        let result = service.three_way_merge(&base, &source, &target);
843        assert!(result.is_ok());
844
845        let (merged, conflicts) = result.unwrap();
846        assert_eq!(conflicts.len(), 1);
847        assert_eq!(conflicts[0].conflict_type, ConflictType::BothAdded);
848    }
849
850    #[test]
851    fn test_three_way_merge_nested_objects() {
852        let pool_fut = setup_test_db();
853        let rt = tokio::runtime::Runtime::new().unwrap();
854        let pool = rt.block_on(pool_fut);
855        let service = MergeService::new(pool);
856
857        let base = json!({
858            "parent": {
859                "child": "value"
860            }
861        });
862        let source = json!({
863            "parent": {
864                "child": "new_value"
865            }
866        });
867        let target = json!({
868            "parent": {
869                "child": "value"
870            }
871        });
872
873        let result = service.three_way_merge(&base, &source, &target);
874        assert!(result.is_ok());
875
876        let (merged, conflicts) = result.unwrap();
877        assert_eq!(merged["parent"]["child"], json!("new_value"));
878        assert!(conflicts.is_empty());
879    }
880
881    #[test]
882    fn test_three_way_merge_arrays_no_conflict() {
883        let pool_fut = setup_test_db();
884        let rt = tokio::runtime::Runtime::new().unwrap();
885        let pool = rt.block_on(pool_fut);
886        let service = MergeService::new(pool);
887
888        let base = json!([1, 2, 3]);
889        let source = json!([1, 2, 3]);
890        let target = json!([1, 2, 3]);
891
892        let result = service.three_way_merge(&base, &source, &target);
893        assert!(result.is_ok());
894
895        let (merged, conflicts) = result.unwrap();
896        assert_eq!(merged, target);
897        assert!(conflicts.is_empty());
898    }
899
900    #[test]
901    fn test_three_way_merge_arrays_conflict() {
902        let pool_fut = setup_test_db();
903        let rt = tokio::runtime::Runtime::new().unwrap();
904        let pool = rt.block_on(pool_fut);
905        let service = MergeService::new(pool);
906
907        let base = json!([1, 2, 3]);
908        let source = json!([1, 2, 4]);
909        let target = json!([1, 2, 5]);
910
911        let result = service.three_way_merge(&base, &source, &target);
912        assert!(result.is_ok());
913
914        let (merged, conflicts) = result.unwrap();
915        assert_eq!(merged, target);
916        assert_eq!(conflicts.len(), 1);
917    }
918
919    #[test]
920    fn test_workspace_merge_new() {
921        let source_ws = Uuid::new_v4();
922        let target_ws = Uuid::new_v4();
923        let base_commit = Uuid::new_v4();
924        let source_commit = Uuid::new_v4();
925        let target_commit = Uuid::new_v4();
926
927        let merge =
928            WorkspaceMerge::new(source_ws, target_ws, base_commit, source_commit, target_commit);
929
930        assert_eq!(merge.source_workspace_id, source_ws);
931        assert_eq!(merge.target_workspace_id, target_ws);
932        assert_eq!(merge.base_commit_id, base_commit);
933        assert_eq!(merge.source_commit_id, source_commit);
934        assert_eq!(merge.target_commit_id, target_commit);
935        assert_eq!(merge.status, MergeStatus::Pending);
936        assert!(merge.merge_commit_id.is_none());
937    }
938
939    #[test]
940    fn test_merge_conflict_types() {
941        assert_eq!(ConflictType::Modified, ConflictType::Modified);
942        assert_eq!(ConflictType::BothAdded, ConflictType::BothAdded);
943        assert_eq!(ConflictType::DeletedModified, ConflictType::DeletedModified);
944
945        assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
946    }
947
948    #[test]
949    fn test_merge_status_equality() {
950        assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
951        assert_eq!(MergeStatus::Conflict, MergeStatus::Conflict);
952        assert_eq!(MergeStatus::Completed, MergeStatus::Completed);
953
954        assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
955    }
956}