Skip to main content

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        // Determine next version from the latest commit in the target workspace
398        let next_version =
399            match self.version_control.get_latest_commit(merge.target_workspace_id).await? {
400                Some(latest) => latest.version + 1,
401                None => 1,
402            };
403
404        // Create merge commit
405        let merge_commit = self
406            .version_control
407            .create_commit(
408                merge.target_workspace_id,
409                user_id,
410                message,
411                Some(merge.target_commit_id),
412                next_version,
413                resolved_state.clone(),
414                serde_json::json!({
415                    "type": "merge",
416                    "source_workspace_id": merge.source_workspace_id,
417                    "source_commit_id": merge.source_commit_id,
418                }),
419            )
420            .await?;
421
422        // Update merge record
423        let now = Utc::now();
424        sqlx::query!(
425            r#"
426            UPDATE workspace_merges
427            SET merge_commit_id = ?, status = ?, merged_by = ?, merged_at = ?
428            WHERE id = ?
429            "#,
430            merge_commit.id,
431            MergeStatus::Completed,
432            user_id,
433            now,
434            merge_id
435        )
436        .execute(&self.db)
437        .await?;
438
439        Ok(merge_commit.id)
440    }
441
442    /// Get a merge by ID
443    pub async fn get_merge(&self, merge_id: Uuid) -> Result<WorkspaceMerge> {
444        let merge_id_str = merge_id.to_string();
445        let row = sqlx::query!(
446            r#"
447            SELECT
448                id,
449                source_workspace_id,
450                target_workspace_id,
451                base_commit_id,
452                source_commit_id,
453                target_commit_id,
454                merge_commit_id,
455                status,
456                conflict_data,
457                merged_by,
458                merged_at,
459                created_at
460            FROM workspace_merges
461            WHERE id = ?
462            "#,
463            merge_id_str
464        )
465        .fetch_optional(&self.db)
466        .await?
467        .ok_or_else(|| CollabError::Internal(format!("Merge not found: {merge_id}")))?;
468
469        Ok(WorkspaceMerge {
470            id: Uuid::parse_str(&row.id)
471                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
472            source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
473                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
474            target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
475                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
476            base_commit_id: Uuid::parse_str(&row.base_commit_id)
477                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
478            source_commit_id: Uuid::parse_str(&row.source_commit_id)
479                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
480            target_commit_id: Uuid::parse_str(&row.target_commit_id)
481                .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
482            merge_commit_id: row.merge_commit_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
483            status: serde_json::from_str(&row.status)
484                .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
485            conflict_data: row.conflict_data.as_ref().and_then(|s| serde_json::from_str(s).ok()),
486            merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
487            merged_at: row
488                .merged_at
489                .as_ref()
490                .map(|s| {
491                    chrono::DateTime::parse_from_rfc3339(s)
492                        .map(|dt| dt.with_timezone(&Utc))
493                        .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))
494                })
495                .transpose()?,
496            created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
497                .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
498                .with_timezone(&Utc),
499        })
500    }
501
502    /// List merges for a workspace
503    pub async fn list_merges(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMerge>> {
504        let rows = sqlx::query!(
505            r#"
506            SELECT
507                id as "id: Uuid",
508                source_workspace_id as "source_workspace_id: Uuid",
509                target_workspace_id as "target_workspace_id: Uuid",
510                base_commit_id as "base_commit_id: Uuid",
511                source_commit_id as "source_commit_id: Uuid",
512                target_commit_id as "target_commit_id: Uuid",
513                merge_commit_id as "merge_commit_id: Uuid",
514                status,
515                conflict_data,
516                merged_by as "merged_by: Uuid",
517                merged_at,
518                created_at
519            FROM workspace_merges
520            WHERE source_workspace_id = ? OR target_workspace_id = ?
521            ORDER BY created_at DESC
522            "#,
523            workspace_id,
524            workspace_id
525        )
526        .fetch_all(&self.db)
527        .await?;
528
529        let merges: Result<Vec<WorkspaceMerge>> = rows
530            .into_iter()
531            .map(|row| {
532                let status = match row.status.as_str() {
533                    "pending" => MergeStatus::Pending,
534                    "in_progress" => MergeStatus::InProgress,
535                    "completed" => MergeStatus::Completed,
536                    "conflict" => MergeStatus::Conflict,
537                    "cancelled" => MergeStatus::Cancelled,
538                    other => return Err(CollabError::Internal(format!("Invalid status: {other}"))),
539                };
540                Ok(WorkspaceMerge {
541                    id: row.id,
542                    source_workspace_id: row.source_workspace_id,
543                    target_workspace_id: row.target_workspace_id,
544                    base_commit_id: row.base_commit_id,
545                    source_commit_id: row.source_commit_id,
546                    target_commit_id: row.target_commit_id,
547                    merge_commit_id: row.merge_commit_id,
548                    status,
549                    conflict_data: row
550                        .conflict_data
551                        .as_ref()
552                        .and_then(|s| serde_json::from_str(s).ok()),
553                    merged_by: row.merged_by,
554                    merged_at: row
555                        .merged_at
556                        .as_ref()
557                        .map(|s| {
558                            chrono::DateTime::parse_from_rfc3339(s)
559                                .map(|dt| dt.with_timezone(&Utc))
560                                .map_err(|e| {
561                                    CollabError::Internal(format!("Invalid timestamp: {e}"))
562                                })
563                        })
564                        .transpose()?,
565                    created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
566                        .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
567                        .with_timezone(&Utc),
568                })
569            })
570            .collect();
571        let merges = merges?;
572
573        Ok(merges)
574    }
575
576    /// Build a set of all ancestor commit IDs by walking the parent chain
577    async fn build_ancestor_set(&self, commit_id: Uuid) -> Result<std::collections::HashSet<Uuid>> {
578        let mut ancestors = std::collections::HashSet::new();
579        let mut current_id = Some(commit_id);
580        let mut visited = std::collections::HashSet::new();
581
582        // Walk the parent chain up to a reasonable depth (prevent infinite loops)
583        let max_depth = 1000;
584        let mut depth = 0;
585
586        while let Some(id) = current_id {
587            if visited.contains(&id) || depth > max_depth {
588                break; // Cycle detected or max depth reached
589            }
590            visited.insert(id);
591            ancestors.insert(id);
592
593            // Get the commit and move to parent
594            match self.version_control.get_commit(id).await {
595                Ok(commit) => {
596                    current_id = commit.parent_id;
597                    depth += 1;
598                }
599                Err(_) => break, // Commit not found, stop walking
600            }
601        }
602
603        Ok(ancestors)
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use serde_json::json;
611    use sqlx::SqlitePool;
612
613    async fn setup_test_db() -> Pool<Sqlite> {
614        let pool = SqlitePool::connect(":memory:").await.unwrap();
615
616        // Create workspace_forks table
617        sqlx::query(
618            r#"
619            CREATE TABLE IF NOT EXISTS workspace_forks (
620                id TEXT PRIMARY KEY,
621                source_workspace_id TEXT NOT NULL,
622                forked_workspace_id TEXT NOT NULL,
623                fork_point_commit_id TEXT,
624                created_at TEXT NOT NULL,
625                created_by TEXT NOT NULL
626            )
627            "#,
628        )
629        .execute(&pool)
630        .await
631        .unwrap();
632
633        // Create workspace_merges table
634        sqlx::query(
635            r#"
636            CREATE TABLE IF NOT EXISTS workspace_merges (
637                id TEXT PRIMARY KEY,
638                source_workspace_id TEXT NOT NULL,
639                target_workspace_id TEXT NOT NULL,
640                base_commit_id TEXT NOT NULL,
641                source_commit_id TEXT NOT NULL,
642                target_commit_id TEXT NOT NULL,
643                merge_commit_id TEXT,
644                status TEXT NOT NULL,
645                conflict_data TEXT,
646                merged_by TEXT,
647                merged_at TEXT,
648                created_at TEXT NOT NULL
649            )
650            "#,
651        )
652        .execute(&pool)
653        .await
654        .unwrap();
655
656        // Create commits table
657        sqlx::query(
658            r#"
659            CREATE TABLE IF NOT EXISTS commits (
660                id TEXT PRIMARY KEY,
661                workspace_id TEXT NOT NULL,
662                user_id TEXT NOT NULL,
663                message TEXT NOT NULL,
664                parent_id TEXT,
665                version INTEGER NOT NULL,
666                snapshot TEXT NOT NULL,
667                metadata TEXT,
668                created_at TEXT NOT NULL
669            )
670            "#,
671        )
672        .execute(&pool)
673        .await
674        .unwrap();
675
676        pool
677    }
678
679    #[tokio::test]
680    async fn test_merge_service_new() {
681        let pool = setup_test_db().await;
682        let _service = MergeService::new(pool);
683        // MergeService was successfully created with test database
684    }
685
686    #[test]
687    fn test_three_way_merge_no_changes() {
688        let pool_fut = setup_test_db();
689        let rt = tokio::runtime::Runtime::new().unwrap();
690        let pool = rt.block_on(pool_fut);
691        let service = MergeService::new(pool);
692
693        let base = json!({"key": "value"});
694        let source = json!({"key": "value"});
695        let target = json!({"key": "value"});
696
697        let result = service.three_way_merge(&base, &source, &target);
698        assert!(result.is_ok());
699
700        let (merged, conflicts) = result.unwrap();
701        assert_eq!(merged, target);
702        assert!(conflicts.is_empty());
703    }
704
705    #[test]
706    fn test_three_way_merge_source_change() {
707        let pool_fut = setup_test_db();
708        let rt = tokio::runtime::Runtime::new().unwrap();
709        let pool = rt.block_on(pool_fut);
710        let service = MergeService::new(pool);
711
712        let base = json!({"key": "value"});
713        let source = json!({"key": "new_value"});
714        let target = json!({"key": "value"});
715
716        let result = service.three_way_merge(&base, &source, &target);
717        assert!(result.is_ok());
718
719        let (merged, conflicts) = result.unwrap();
720        assert_eq!(merged, source);
721        assert!(conflicts.is_empty());
722    }
723
724    #[test]
725    fn test_three_way_merge_target_change() {
726        let pool_fut = setup_test_db();
727        let rt = tokio::runtime::Runtime::new().unwrap();
728        let pool = rt.block_on(pool_fut);
729        let service = MergeService::new(pool);
730
731        let base = json!({"key": "value"});
732        let source = json!({"key": "value"});
733        let target = json!({"key": "new_value"});
734
735        let result = service.three_way_merge(&base, &source, &target);
736        assert!(result.is_ok());
737
738        let (merged, conflicts) = result.unwrap();
739        assert_eq!(merged, target);
740        assert!(conflicts.is_empty());
741    }
742
743    #[test]
744    fn test_three_way_merge_both_changed_same() {
745        let pool_fut = setup_test_db();
746        let rt = tokio::runtime::Runtime::new().unwrap();
747        let pool = rt.block_on(pool_fut);
748        let service = MergeService::new(pool);
749
750        let base = json!({"key": "value"});
751        let source = json!({"key": "new_value"});
752        let target = json!({"key": "new_value"});
753
754        let result = service.three_way_merge(&base, &source, &target);
755        assert!(result.is_ok());
756
757        let (merged, conflicts) = result.unwrap();
758        assert_eq!(merged, source);
759        assert!(conflicts.is_empty());
760    }
761
762    #[test]
763    fn test_three_way_merge_conflict() {
764        let pool_fut = setup_test_db();
765        let rt = tokio::runtime::Runtime::new().unwrap();
766        let pool = rt.block_on(pool_fut);
767        let service = MergeService::new(pool);
768
769        let base = json!({"key": "value"});
770        let source = json!({"key": "source_value"});
771        let target = json!({"key": "target_value"});
772
773        let result = service.three_way_merge(&base, &source, &target);
774        assert!(result.is_ok());
775
776        let (merged, conflicts) = result.unwrap();
777        assert_eq!(merged, target); // Target is kept on conflict
778        assert_eq!(conflicts.len(), 1);
779        assert_eq!(conflicts[0].path, "key");
780        assert_eq!(conflicts[0].conflict_type, ConflictType::Modified);
781    }
782
783    #[test]
784    fn test_three_way_merge_object_add_source() {
785        let pool_fut = setup_test_db();
786        let rt = tokio::runtime::Runtime::new().unwrap();
787        let pool = rt.block_on(pool_fut);
788        let service = MergeService::new(pool);
789
790        let base = json!({});
791        let source = json!({"new_key": "value"});
792        let target = json!({});
793
794        let result = service.three_way_merge(&base, &source, &target);
795        assert!(result.is_ok());
796
797        let (merged, conflicts) = result.unwrap();
798        assert_eq!(merged.get("new_key"), Some(&json!("value")));
799        assert!(conflicts.is_empty());
800    }
801
802    #[test]
803    fn test_three_way_merge_object_add_target() {
804        let pool_fut = setup_test_db();
805        let rt = tokio::runtime::Runtime::new().unwrap();
806        let pool = rt.block_on(pool_fut);
807        let service = MergeService::new(pool);
808
809        let base = json!({});
810        let source = json!({});
811        let target = json!({"new_key": "value"});
812
813        let result = service.three_way_merge(&base, &source, &target);
814        assert!(result.is_ok());
815
816        let (merged, conflicts) = result.unwrap();
817        assert_eq!(merged.get("new_key"), Some(&json!("value")));
818        assert!(conflicts.is_empty());
819    }
820
821    #[test]
822    fn test_three_way_merge_both_added_different() {
823        let pool_fut = setup_test_db();
824        let rt = tokio::runtime::Runtime::new().unwrap();
825        let pool = rt.block_on(pool_fut);
826        let service = MergeService::new(pool);
827
828        let base = json!({});
829        let source = json!({"key": "source_value"});
830        let target = json!({"key": "target_value"});
831
832        let result = service.three_way_merge(&base, &source, &target);
833        assert!(result.is_ok());
834
835        let (merged, conflicts) = result.unwrap();
836        assert_eq!(conflicts.len(), 1);
837        assert_eq!(conflicts[0].conflict_type, ConflictType::BothAdded);
838    }
839
840    #[test]
841    fn test_three_way_merge_nested_objects() {
842        let pool_fut = setup_test_db();
843        let rt = tokio::runtime::Runtime::new().unwrap();
844        let pool = rt.block_on(pool_fut);
845        let service = MergeService::new(pool);
846
847        let base = json!({
848            "parent": {
849                "child": "value"
850            }
851        });
852        let source = json!({
853            "parent": {
854                "child": "new_value"
855            }
856        });
857        let target = json!({
858            "parent": {
859                "child": "value"
860            }
861        });
862
863        let result = service.three_way_merge(&base, &source, &target);
864        assert!(result.is_ok());
865
866        let (merged, conflicts) = result.unwrap();
867        assert_eq!(merged["parent"]["child"], json!("new_value"));
868        assert!(conflicts.is_empty());
869    }
870
871    #[test]
872    fn test_three_way_merge_arrays_no_conflict() {
873        let pool_fut = setup_test_db();
874        let rt = tokio::runtime::Runtime::new().unwrap();
875        let pool = rt.block_on(pool_fut);
876        let service = MergeService::new(pool);
877
878        let base = json!([1, 2, 3]);
879        let source = json!([1, 2, 3]);
880        let target = json!([1, 2, 3]);
881
882        let result = service.three_way_merge(&base, &source, &target);
883        assert!(result.is_ok());
884
885        let (merged, conflicts) = result.unwrap();
886        assert_eq!(merged, target);
887        assert!(conflicts.is_empty());
888    }
889
890    #[test]
891    fn test_three_way_merge_arrays_conflict() {
892        let pool_fut = setup_test_db();
893        let rt = tokio::runtime::Runtime::new().unwrap();
894        let pool = rt.block_on(pool_fut);
895        let service = MergeService::new(pool);
896
897        let base = json!([1, 2, 3]);
898        let source = json!([1, 2, 4]);
899        let target = json!([1, 2, 5]);
900
901        let result = service.three_way_merge(&base, &source, &target);
902        assert!(result.is_ok());
903
904        let (merged, conflicts) = result.unwrap();
905        assert_eq!(merged, target);
906        assert_eq!(conflicts.len(), 1);
907    }
908
909    #[test]
910    fn test_workspace_merge_new() {
911        let source_ws = Uuid::new_v4();
912        let target_ws = Uuid::new_v4();
913        let base_commit = Uuid::new_v4();
914        let source_commit = Uuid::new_v4();
915        let target_commit = Uuid::new_v4();
916
917        let merge =
918            WorkspaceMerge::new(source_ws, target_ws, base_commit, source_commit, target_commit);
919
920        assert_eq!(merge.source_workspace_id, source_ws);
921        assert_eq!(merge.target_workspace_id, target_ws);
922        assert_eq!(merge.base_commit_id, base_commit);
923        assert_eq!(merge.source_commit_id, source_commit);
924        assert_eq!(merge.target_commit_id, target_commit);
925        assert_eq!(merge.status, MergeStatus::Pending);
926        assert!(merge.merge_commit_id.is_none());
927    }
928
929    #[test]
930    fn test_merge_conflict_types() {
931        assert_eq!(ConflictType::Modified, ConflictType::Modified);
932        assert_eq!(ConflictType::BothAdded, ConflictType::BothAdded);
933        assert_eq!(ConflictType::DeletedModified, ConflictType::DeletedModified);
934
935        assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
936    }
937
938    #[test]
939    fn test_merge_status_equality() {
940        assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
941        assert_eq!(MergeStatus::Conflict, MergeStatus::Conflict);
942        assert_eq!(MergeStatus::Completed, MergeStatus::Completed);
943
944        assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
945    }
946}