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