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