cascade_cli/stack/
manager.rs

1use super::metadata::RepositoryMetadata;
2use super::{CommitMetadata, Stack, StackEntry, StackMetadata, StackStatus};
3use crate::cli::output::Output;
4use crate::config::{get_repo_config_dir, Settings};
5use crate::errors::{CascadeError, Result};
6use crate::git::GitRepository;
7use dialoguer::{theme::ColorfulTheme, Input, Select};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11use tracing::info;
12use uuid::Uuid;
13
14/// Types of branch modifications detected during Git integrity checks
15#[derive(Debug)]
16pub enum BranchModification {
17    /// Branch is missing (needs to be created)
18    Missing {
19        branch: String,
20        entry_id: Uuid,
21        expected_commit: String,
22    },
23    /// Branch has extra commits beyond what's expected
24    ExtraCommits {
25        branch: String,
26        entry_id: Uuid,
27        expected_commit: String,
28        actual_commit: String,
29        extra_commit_count: usize,
30        extra_commit_messages: Vec<String>,
31    },
32}
33
34/// Manages all stack operations and persistence
35pub struct StackManager {
36    /// Git repository interface
37    repo: GitRepository,
38    /// Path to the repository root
39    repo_path: PathBuf,
40    /// Path to cascade config directory
41    config_dir: PathBuf,
42    /// Path to stacks data file
43    stacks_file: PathBuf,
44    /// Path to metadata file
45    metadata_file: PathBuf,
46    /// In-memory stack data
47    stacks: HashMap<Uuid, Stack>,
48    /// Repository metadata
49    metadata: RepositoryMetadata,
50}
51
52impl StackManager {
53    /// Create a new StackManager for the given repository
54    pub fn new(repo_path: &Path) -> Result<Self> {
55        let repo = GitRepository::open(repo_path)?;
56        let config_dir = get_repo_config_dir(repo_path)?;
57        let stacks_file = config_dir.join("stacks.json");
58        let metadata_file = config_dir.join("metadata.json");
59
60        // Load configuration to get the configured default branch
61        let config_file = config_dir.join("config.json");
62        let settings = Settings::load_from_file(&config_file).unwrap_or_default();
63        let configured_default = &settings.git.default_branch;
64
65        // Using configured default branch
66
67        // Determine default base branch - use configured default if it exists
68        let default_base = if repo.branch_exists(configured_default) {
69            // Found configured default branch locally
70            configured_default.clone()
71        } else {
72            // Fall back to detecting a suitable branch
73            match repo.detect_main_branch() {
74                Ok(detected) => {
75                    // Configured default branch not found, using detected branch
76                    detected
77                }
78                Err(_) => {
79                    // Use configured default even if it doesn't exist yet (might be created later)
80                    // Using configured default branch even though it doesn't exist locally
81                    configured_default.clone()
82                }
83            }
84        };
85
86        let mut manager = Self {
87            repo,
88            repo_path: repo_path.to_path_buf(),
89            config_dir,
90            stacks_file,
91            metadata_file,
92            stacks: HashMap::new(),
93            metadata: RepositoryMetadata::new(default_base),
94        };
95
96        // Load existing data if available
97        manager.load_from_disk()?;
98
99        Ok(manager)
100    }
101
102    /// Create a new stack
103    pub fn create_stack(
104        &mut self,
105        name: String,
106        base_branch: Option<String>,
107        description: Option<String>,
108    ) -> Result<Uuid> {
109        // Check if stack with this name already exists
110        if self.metadata.find_stack_by_name(&name).is_some() {
111            return Err(CascadeError::config(format!(
112                "Stack '{name}' already exists"
113            )));
114        }
115
116        // Use provided base branch or default
117        let base_branch = base_branch.unwrap_or_else(|| self.metadata.default_base_branch.clone());
118
119        // Verify base branch exists (try to fetch from remote if not local)
120        if !self.repo.branch_exists_or_fetch(&base_branch)? {
121            return Err(CascadeError::branch(format!(
122                "Base branch '{base_branch}' does not exist locally or remotely"
123            )));
124        }
125
126        // Get current branch as the working branch
127        let current_branch = self.repo.get_current_branch().ok();
128
129        // Create the stack
130        let mut stack = Stack::new(name.clone(), base_branch.clone(), description.clone());
131
132        // Set working branch if we're on a feature branch (not on base branch)
133        if let Some(ref branch) = current_branch {
134            if branch != &base_branch {
135                stack.working_branch = Some(branch.clone());
136            }
137        }
138
139        let stack_id = stack.id;
140
141        // Create metadata
142        let stack_metadata = StackMetadata::new(stack_id, name, base_branch, description);
143
144        // Store in memory
145        self.stacks.insert(stack_id, stack);
146        self.metadata.add_stack(stack_metadata);
147
148        // Always set newly created stack as active
149        self.set_active_stack(Some(stack_id))?;
150
151        Ok(stack_id)
152    }
153
154    /// Get a stack by ID
155    pub fn get_stack(&self, stack_id: &Uuid) -> Option<&Stack> {
156        self.stacks.get(stack_id)
157    }
158
159    /// Get a mutable stack by ID
160    pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut Stack> {
161        self.stacks.get_mut(stack_id)
162    }
163
164    /// Get stack by name
165    pub fn get_stack_by_name(&self, name: &str) -> Option<&Stack> {
166        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
167            self.stacks.get(&metadata.stack_id)
168        } else {
169            None
170        }
171    }
172
173    /// Get mutable stack by name
174    pub fn get_stack_by_name_mut(&mut self, name: &str) -> Option<&mut Stack> {
175        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
176            self.stacks.get_mut(&metadata.stack_id)
177        } else {
178            None
179        }
180    }
181
182    /// Update working branch for a stack
183    pub fn update_stack_working_branch(&mut self, name: &str, branch: String) -> Result<()> {
184        if let Some(stack) = self.get_stack_by_name_mut(name) {
185            stack.working_branch = Some(branch);
186            self.save_to_disk()?;
187            Ok(())
188        } else {
189            Err(CascadeError::config(format!("Stack '{name}' not found")))
190        }
191    }
192
193    /// Get the currently active stack
194    pub fn get_active_stack(&self) -> Option<&Stack> {
195        self.metadata
196            .active_stack_id
197            .and_then(|id| self.stacks.get(&id))
198    }
199
200    /// Get the currently active stack mutably
201    pub fn get_active_stack_mut(&mut self) -> Option<&mut Stack> {
202        if let Some(id) = self.metadata.active_stack_id {
203            self.stacks.get_mut(&id)
204        } else {
205            None
206        }
207    }
208
209    /// Set the active stack
210    pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) -> Result<()> {
211        // Verify stack exists if provided
212        if let Some(id) = stack_id {
213            if !self.stacks.contains_key(&id) {
214                return Err(CascadeError::config(format!(
215                    "Stack with ID {id} not found"
216                )));
217            }
218        }
219
220        // Update active flag on stacks
221        for stack in self.stacks.values_mut() {
222            stack.set_active(Some(stack.id) == stack_id);
223        }
224
225        // Track the current branch when activating a stack
226        if let Some(id) = stack_id {
227            let current_branch = self.repo.get_current_branch().ok();
228            if let Some(stack_meta) = self.metadata.get_stack_mut(&id) {
229                stack_meta.set_current_branch(current_branch);
230            }
231        }
232
233        self.metadata.set_active_stack(stack_id);
234        self.save_to_disk()?;
235
236        Ok(())
237    }
238
239    /// Set active stack by name
240    pub fn set_active_stack_by_name(&mut self, name: &str) -> Result<()> {
241        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
242            self.set_active_stack(Some(metadata.stack_id))
243        } else {
244            Err(CascadeError::config(format!("Stack '{name}' not found")))
245        }
246    }
247
248    /// Delete a stack
249    pub fn delete_stack(&mut self, stack_id: &Uuid) -> Result<Stack> {
250        let stack = self
251            .stacks
252            .remove(stack_id)
253            .ok_or_else(|| CascadeError::config(format!("Stack with ID {stack_id} not found")))?;
254
255        // Remove metadata
256        self.metadata.remove_stack(stack_id);
257
258        // Remove all associated commit metadata
259        let stack_commits: Vec<String> = self
260            .metadata
261            .commits
262            .values()
263            .filter(|commit| &commit.stack_id == stack_id)
264            .map(|commit| commit.hash.clone())
265            .collect();
266
267        for commit_hash in stack_commits {
268            self.metadata.remove_commit(&commit_hash);
269        }
270
271        // If this was the active stack, find a new one
272        if self.metadata.active_stack_id == Some(*stack_id) {
273            let new_active = self.metadata.stacks.keys().next().copied();
274            self.set_active_stack(new_active)?;
275        }
276
277        self.save_to_disk()?;
278
279        Ok(stack)
280    }
281
282    /// Push a commit to a stack
283    pub fn push_to_stack(
284        &mut self,
285        branch: String,
286        commit_hash: String,
287        message: String,
288        source_branch: String,
289    ) -> Result<Uuid> {
290        let stack_id = self
291            .metadata
292            .active_stack_id
293            .ok_or_else(|| CascadeError::config("No active stack"))?;
294
295        let stack = self
296            .stacks
297            .get_mut(&stack_id)
298            .ok_or_else(|| CascadeError::config("Active stack not found"))?;
299
300        // šŸ†• VALIDATE GIT INTEGRITY BEFORE PUSHING (if stack is not empty)
301        if !stack.entries.is_empty() {
302            if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
303                return Err(CascadeError::validation(format!(
304                    "Cannot push to corrupted stack '{}':\n{}\n\n\
305                     šŸ’” Fix the stack integrity issues first using 'ca stack validate {}' for details.",
306                    stack.name, integrity_error, stack.name
307                )));
308            }
309        }
310
311        // Verify the commit exists
312        if !self.repo.commit_exists(&commit_hash)? {
313            return Err(CascadeError::branch(format!(
314                "Commit {commit_hash} does not exist"
315            )));
316        }
317
318        // Check for duplicate commit messages within the same stack
319        if let Some(duplicate_entry) = stack.entries.iter().find(|entry| entry.message == message) {
320            return Err(CascadeError::validation(format!(
321                "Duplicate commit message in stack: \"{message}\"\n\n\
322                 This message already exists in entry {} (commit: {})\n\n\
323                 šŸ’” Consider using a more specific message:\n\
324                    • Add context: \"{message} - add validation\"\n\
325                    • Be more specific: \"Fix user authentication timeout bug\"\n\
326                    • Or amend the previous commit: git commit --amend",
327                duplicate_entry.id,
328                &duplicate_entry.commit_hash[..8]
329            )));
330        }
331
332        // šŸŽÆ SMART BASE BRANCH UPDATE FOR FEATURE WORKFLOW
333        // If this is the first commit in an empty stack, and the user is on a feature branch
334        // that's different from the stack's base branch, update the base branch to match
335        // the current workflow.
336        if stack.entries.is_empty() {
337            let current_branch = self.repo.get_current_branch()?;
338
339            // Update working branch if not already set
340            if stack.working_branch.is_none() && current_branch != stack.base_branch {
341                stack.working_branch = Some(current_branch.clone());
342                tracing::info!(
343                    "Set working branch for stack '{}' to '{}'",
344                    stack.name,
345                    current_branch
346                );
347            }
348
349            if current_branch != stack.base_branch && current_branch != "HEAD" {
350                // Check if current branch was created from the stack's base branch
351                let base_exists = self.repo.branch_exists(&stack.base_branch);
352                let current_is_feature = current_branch.starts_with("feature/")
353                    || current_branch.starts_with("fix/")
354                    || current_branch.starts_with("chore/")
355                    || current_branch.contains("feature")
356                    || current_branch.contains("fix");
357
358                if base_exists && current_is_feature {
359                    tracing::info!(
360                        "šŸŽÆ First commit detected: updating stack '{}' base branch from '{}' to '{}'",
361                        stack.name, stack.base_branch, current_branch
362                    );
363
364                    Output::info("šŸŽÆ Smart Base Branch Update:");
365                    Output::sub_item(format!(
366                        "Stack '{}' was created with base '{}'",
367                        stack.name, stack.base_branch
368                    ));
369                    Output::sub_item(format!(
370                        "You're now working on feature branch '{current_branch}'"
371                    ));
372                    Output::sub_item("Updating stack base branch to match your workflow");
373
374                    // Update the stack's base branch
375                    stack.base_branch = current_branch.clone();
376
377                    // Update metadata as well
378                    if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
379                        stack_meta.base_branch = current_branch.clone();
380                        stack_meta.set_current_branch(Some(current_branch.clone()));
381                    }
382
383                    println!(
384                        "   āœ… Stack '{}' base branch updated to '{current_branch}'",
385                        stack.name
386                    );
387                }
388            }
389        }
390
391        // šŸ†• CREATE ACTUAL GIT BRANCH from the specific commit
392        // Check if branch already exists
393        if self.repo.branch_exists(&branch) {
394            // Branch already exists, skip creation
395        } else {
396            // Create the branch from the specific commit hash
397            self.repo
398                .create_branch(&branch, Some(&commit_hash))
399                .map_err(|e| {
400                    CascadeError::branch(format!(
401                        "Failed to create branch '{}' from commit {}: {}",
402                        branch,
403                        &commit_hash[..8],
404                        e
405                    ))
406                })?;
407
408            // Branch creation succeeded - logging handled by caller
409        }
410
411        // Add to stack
412        let entry_id = stack.push_entry(branch.clone(), commit_hash.clone(), message.clone());
413
414        // Create commit metadata
415        let commit_metadata = CommitMetadata::new(
416            commit_hash.clone(),
417            message,
418            entry_id,
419            stack_id,
420            branch.clone(),
421            source_branch,
422        );
423
424        // Update repository metadata
425        self.metadata.add_commit(commit_metadata);
426        if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
427            stack_meta.add_branch(branch);
428            stack_meta.add_commit(commit_hash);
429        }
430
431        self.save_to_disk()?;
432
433        Ok(entry_id)
434    }
435
436    /// Pop the top commit from the active stack
437    pub fn pop_from_stack(&mut self) -> Result<StackEntry> {
438        let stack_id = self
439            .metadata
440            .active_stack_id
441            .ok_or_else(|| CascadeError::config("No active stack"))?;
442
443        let stack = self
444            .stacks
445            .get_mut(&stack_id)
446            .ok_or_else(|| CascadeError::config("Active stack not found"))?;
447
448        let entry = stack
449            .pop_entry()
450            .ok_or_else(|| CascadeError::config("Stack is empty"))?;
451
452        // Remove commit metadata
453        self.metadata.remove_commit(&entry.commit_hash);
454
455        // Update stack metadata
456        if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
457            stack_meta.remove_commit(&entry.commit_hash);
458            // Note: We don't remove the branch as there might be other commits on it
459        }
460
461        self.save_to_disk()?;
462
463        Ok(entry)
464    }
465
466    /// Submit a stack entry for review (mark as submitted)
467    pub fn submit_entry(
468        &mut self,
469        stack_id: &Uuid,
470        entry_id: &Uuid,
471        pull_request_id: String,
472    ) -> Result<()> {
473        let stack = self
474            .stacks
475            .get_mut(stack_id)
476            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
477
478        let entry_commit_hash = {
479            let entry = stack
480                .get_entry(entry_id)
481                .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
482            entry.commit_hash.clone()
483        };
484
485        // Update stack entry
486        if !stack.mark_entry_submitted(entry_id, pull_request_id.clone()) {
487            return Err(CascadeError::config(format!(
488                "Failed to mark entry {entry_id} as submitted"
489            )));
490        }
491
492        // Update commit metadata
493        if let Some(commit_meta) = self.metadata.commits.get_mut(&entry_commit_hash) {
494            commit_meta.mark_submitted(pull_request_id);
495        }
496
497        // Update stack metadata statistics
498        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
499            let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
500            stack_meta.update_stats(
501                stack.entries.len(),
502                submitted_count,
503                stack_meta.merged_commits,
504            );
505        }
506
507        self.save_to_disk()?;
508
509        Ok(())
510    }
511
512    /// Repair data consistency issues in all stacks
513    pub fn repair_all_stacks(&mut self) -> Result<()> {
514        for stack in self.stacks.values_mut() {
515            stack.repair_data_consistency();
516        }
517        self.save_to_disk()?;
518        Ok(())
519    }
520
521    /// Get all stacks
522    pub fn get_all_stacks(&self) -> Vec<&Stack> {
523        self.stacks.values().collect()
524    }
525
526    /// Get stack metadata
527    pub fn get_stack_metadata(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
528        self.metadata.get_stack(stack_id)
529    }
530
531    /// Get repository metadata
532    pub fn get_repository_metadata(&self) -> &RepositoryMetadata {
533        &self.metadata
534    }
535
536    /// Get the Git repository
537    pub fn git_repo(&self) -> &GitRepository {
538        &self.repo
539    }
540
541    /// Get the repository path
542    pub fn repo_path(&self) -> &Path {
543        &self.repo_path
544    }
545
546    // Edit mode management methods
547
548    /// Check if currently in edit mode
549    pub fn is_in_edit_mode(&self) -> bool {
550        self.metadata
551            .edit_mode
552            .as_ref()
553            .map(|edit_state| edit_state.is_active)
554            .unwrap_or(false)
555    }
556
557    /// Get current edit mode information
558    pub fn get_edit_mode_info(&self) -> Option<&super::metadata::EditModeState> {
559        self.metadata.edit_mode.as_ref()
560    }
561
562    /// Enter edit mode for a specific stack entry
563    pub fn enter_edit_mode(&mut self, stack_id: Uuid, entry_id: Uuid) -> Result<()> {
564        // Get the commit hash first to avoid borrow checker issues
565        let commit_hash = {
566            let stack = self
567                .get_stack(&stack_id)
568                .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
569
570            let entry = stack.get_entry(&entry_id).ok_or_else(|| {
571                CascadeError::config(format!("Entry {entry_id} not found in stack"))
572            })?;
573
574            entry.commit_hash.clone()
575        };
576
577        // If already in edit mode, exit the current one first
578        if self.is_in_edit_mode() {
579            self.exit_edit_mode()?;
580        }
581
582        // Create new edit mode state
583        let edit_state = super::metadata::EditModeState::new(stack_id, entry_id, commit_hash);
584
585        self.metadata.edit_mode = Some(edit_state);
586        self.save_to_disk()?;
587
588        info!(
589            "Entered edit mode for entry {} in stack {}",
590            entry_id, stack_id
591        );
592        Ok(())
593    }
594
595    /// Exit edit mode
596    pub fn exit_edit_mode(&mut self) -> Result<()> {
597        if !self.is_in_edit_mode() {
598            return Err(CascadeError::config("Not currently in edit mode"));
599        }
600
601        // Clear edit mode state
602        self.metadata.edit_mode = None;
603        self.save_to_disk()?;
604
605        info!("Exited edit mode");
606        Ok(())
607    }
608
609    /// Sync stack with Git repository state
610    pub fn sync_stack(&mut self, stack_id: &Uuid) -> Result<()> {
611        let stack = self
612            .stacks
613            .get_mut(stack_id)
614            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
615
616        // šŸ†• ENHANCED: Check Git integrity first (branch HEAD matches stored commits)
617        if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
618            stack.update_status(StackStatus::Corrupted);
619            return Err(CascadeError::branch(format!(
620                "Stack '{}' Git integrity check failed:\n{}",
621                stack.name, integrity_error
622            )));
623        }
624
625        // Check if all commits still exist
626        let mut missing_commits = Vec::new();
627        for entry in &stack.entries {
628            if !self.repo.commit_exists(&entry.commit_hash)? {
629                missing_commits.push(entry.commit_hash.clone());
630            }
631        }
632
633        if !missing_commits.is_empty() {
634            stack.update_status(StackStatus::Corrupted);
635            return Err(CascadeError::branch(format!(
636                "Stack {} has missing commits: {}",
637                stack.name,
638                missing_commits.join(", ")
639            )));
640        }
641
642        // Check if base branch exists and has new commits (try to fetch from remote if not local)
643        if !self.repo.branch_exists_or_fetch(&stack.base_branch)? {
644            return Err(CascadeError::branch(format!(
645                "Base branch '{}' does not exist locally or remotely. Check the branch name or switch to a different base.",
646                stack.base_branch
647            )));
648        }
649
650        let _base_hash = self.repo.get_branch_head(&stack.base_branch)?;
651
652        // Check if any stack entries are missing their commits
653        let mut corrupted_entry = None;
654        for entry in &stack.entries {
655            if !self.repo.commit_exists(&entry.commit_hash)? {
656                corrupted_entry = Some((entry.commit_hash.clone(), entry.branch.clone()));
657                break;
658            }
659        }
660
661        if let Some((commit_hash, branch)) = corrupted_entry {
662            stack.update_status(StackStatus::Corrupted);
663            return Err(CascadeError::branch(format!(
664                "Commit {commit_hash} from stack entry '{branch}' no longer exists"
665            )));
666        }
667
668        // Compare base branch with the earliest commit in the stack
669        let needs_sync = if let Some(first_entry) = stack.entries.first() {
670            // Get commits between base and first entry
671            match self
672                .repo
673                .get_commits_between(&stack.base_branch, &first_entry.commit_hash)
674            {
675                Ok(commits) => !commits.is_empty(), // If there are commits, we need to sync
676                Err(_) => true,                     // If we can't compare, assume we need to sync
677            }
678        } else {
679            false // Empty stack is always clean
680        };
681
682        // Update stack status based on sync needs
683        if needs_sync {
684            stack.update_status(StackStatus::NeedsSync);
685            info!(
686                "Stack '{}' needs sync - new commits on base branch",
687                stack.name
688            );
689        } else {
690            stack.update_status(StackStatus::Clean);
691            info!("Stack '{}' is clean", stack.name);
692        }
693
694        // Update metadata
695        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
696            stack_meta.set_up_to_date(true);
697        }
698
699        self.save_to_disk()?;
700
701        Ok(())
702    }
703
704    /// List all stacks with their status
705    pub fn list_stacks(&self) -> Vec<(Uuid, &str, &StackStatus, usize, Option<&str>)> {
706        self.stacks
707            .values()
708            .map(|stack| {
709                (
710                    stack.id,
711                    stack.name.as_str(),
712                    &stack.status,
713                    stack.entries.len(),
714                    if stack.is_active {
715                        Some("active")
716                    } else {
717                        None
718                    },
719                )
720            })
721            .collect()
722    }
723
724    /// Get all stacks as Stack objects for TUI
725    pub fn get_all_stacks_objects(&self) -> Result<Vec<Stack>> {
726        let mut stacks: Vec<Stack> = self.stacks.values().cloned().collect();
727        stacks.sort_by(|a, b| a.name.cmp(&b.name));
728        Ok(stacks)
729    }
730
731    /// Validate all stacks including Git integrity
732    pub fn validate_all(&self) -> Result<()> {
733        for stack in self.stacks.values() {
734            // Basic structure validation
735            stack.validate().map_err(|e| {
736                CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
737            })?;
738
739            // Git integrity validation
740            stack.validate_git_integrity(&self.repo).map_err(|e| {
741                CascadeError::config(format!(
742                    "Stack '{}' Git integrity validation failed: {}",
743                    stack.name, e
744                ))
745            })?;
746        }
747        Ok(())
748    }
749
750    /// Validate a specific stack including Git integrity
751    pub fn validate_stack(&self, stack_id: &Uuid) -> Result<()> {
752        let stack = self
753            .stacks
754            .get(stack_id)
755            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
756
757        // Basic structure validation
758        stack.validate().map_err(|e| {
759            CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
760        })?;
761
762        // Git integrity validation
763        stack.validate_git_integrity(&self.repo).map_err(|e| {
764            CascadeError::config(format!(
765                "Stack '{}' Git integrity validation failed: {}",
766                stack.name, e
767            ))
768        })?;
769
770        Ok(())
771    }
772
773    /// Save all data to disk
774    fn save_to_disk(&self) -> Result<()> {
775        // Ensure config directory exists
776        if !self.config_dir.exists() {
777            fs::create_dir_all(&self.config_dir).map_err(|e| {
778                CascadeError::config(format!("Failed to create config directory: {e}"))
779            })?;
780        }
781
782        // Save stacks atomically
783        crate::utils::atomic_file::write_json(&self.stacks_file, &self.stacks)?;
784
785        // Save metadata atomically
786        crate::utils::atomic_file::write_json(&self.metadata_file, &self.metadata)?;
787
788        Ok(())
789    }
790
791    /// Load data from disk
792    fn load_from_disk(&mut self) -> Result<()> {
793        // Load stacks if file exists
794        if self.stacks_file.exists() {
795            let stacks_content = fs::read_to_string(&self.stacks_file)
796                .map_err(|e| CascadeError::config(format!("Failed to read stacks file: {e}")))?;
797
798            self.stacks = serde_json::from_str(&stacks_content)
799                .map_err(|e| CascadeError::config(format!("Failed to parse stacks file: {e}")))?;
800        }
801
802        // Load metadata if file exists
803        if self.metadata_file.exists() {
804            let metadata_content = fs::read_to_string(&self.metadata_file)
805                .map_err(|e| CascadeError::config(format!("Failed to read metadata file: {e}")))?;
806
807            self.metadata = serde_json::from_str(&metadata_content)
808                .map_err(|e| CascadeError::config(format!("Failed to parse metadata file: {e}")))?;
809        }
810
811        Ok(())
812    }
813
814    /// Check if the user has changed branches since the stack was activated
815    /// Returns true if branch change detected and user wants to proceed
816    pub fn check_for_branch_change(&mut self) -> Result<bool> {
817        // Extract stack information first to avoid borrow conflicts
818        let (stack_id, stack_name, stored_branch) = {
819            if let Some(active_stack) = self.get_active_stack() {
820                let stack_id = active_stack.id;
821                let stack_name = active_stack.name.clone();
822                let stored_branch = if let Some(stack_meta) = self.metadata.get_stack(&stack_id) {
823                    stack_meta.current_branch.clone()
824                } else {
825                    None
826                };
827                (Some(stack_id), stack_name, stored_branch)
828            } else {
829                (None, String::new(), None)
830            }
831        };
832
833        // If no active stack, nothing to check
834        let Some(stack_id) = stack_id else {
835            return Ok(true);
836        };
837
838        let current_branch = self.repo.get_current_branch().ok();
839
840        // Check if branch has changed
841        if stored_branch.as_ref() != current_branch.as_ref() {
842            Output::warning("Branch change detected!");
843            Output::sub_item(format!(
844                "Stack '{}' was active on: {}",
845                stack_name,
846                stored_branch.as_deref().unwrap_or("unknown")
847            ));
848            Output::sub_item(format!(
849                "Current branch: {}",
850                current_branch.as_deref().unwrap_or("unknown")
851            ));
852            Output::spacing();
853
854            let options = vec![
855                format!("Keep stack '{stack_name}' active (continue with stack workflow)"),
856                "Deactivate stack (use normal Git workflow)".to_string(),
857                "Switch to a different stack".to_string(),
858                "Cancel and stay on current workflow".to_string(),
859            ];
860
861            let choice = Select::with_theme(&ColorfulTheme::default())
862                .with_prompt("What would you like to do?")
863                .default(0)
864                .items(&options)
865                .interact()
866                .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
867
868            match choice {
869                0 => {
870                    // Update the tracked branch and continue
871                    if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
872                        stack_meta.set_current_branch(current_branch);
873                    }
874                    self.save_to_disk()?;
875                    Output::success(format!(
876                        "Continuing with stack '{stack_name}' on current branch"
877                    ));
878                    return Ok(true);
879                }
880                1 => {
881                    // Deactivate the stack
882                    self.set_active_stack(None)?;
883                    Output::success(format!(
884                        "Deactivated stack '{stack_name}' - using normal Git workflow"
885                    ));
886                    return Ok(false);
887                }
888                2 => {
889                    // Show available stacks
890                    let stacks = self.get_all_stacks();
891                    if stacks.len() <= 1 {
892                        Output::warning("No other stacks available. Deactivating current stack.");
893                        self.set_active_stack(None)?;
894                        return Ok(false);
895                    }
896
897                    Output::spacing();
898                    Output::info("Available stacks:");
899                    for (i, stack) in stacks.iter().enumerate() {
900                        if stack.id != stack_id {
901                            Output::numbered_item(i + 1, &stack.name);
902                        }
903                    }
904                    let stack_name_input: String = Input::with_theme(&ColorfulTheme::default())
905                        .with_prompt("Enter stack name")
906                        .validate_with(|input: &String| -> std::result::Result<(), &str> {
907                            if input.trim().is_empty() {
908                                Err("Stack name cannot be empty")
909                            } else {
910                                Ok(())
911                            }
912                        })
913                        .interact_text()
914                        .map_err(|e| {
915                            CascadeError::config(format!("Failed to get user input: {e}"))
916                        })?;
917                    let stack_name_input = stack_name_input.trim();
918
919                    if let Err(e) = self.set_active_stack_by_name(stack_name_input) {
920                        Output::warning(format!("{e}"));
921                        Output::sub_item("Deactivating stack instead.");
922                        self.set_active_stack(None)?;
923                        return Ok(false);
924                    } else {
925                        Output::success(format!("Switched to stack '{stack_name_input}'"));
926                        return Ok(true);
927                    }
928                }
929                3 => {
930                    Output::info("Cancelled - no changes made");
931                    return Ok(false);
932                }
933                _ => {
934                    Output::info("Invalid choice - no changes made");
935                    return Ok(false);
936                }
937            }
938        }
939
940        // No branch change detected
941        Ok(true)
942    }
943
944    /// Handle Git integrity issues with multiple user-friendly options
945    /// Provides non-destructive choices for dealing with branch modifications
946    pub fn handle_branch_modifications(
947        &mut self,
948        stack_id: &Uuid,
949        auto_mode: Option<String>,
950    ) -> Result<()> {
951        let stack = self
952            .stacks
953            .get_mut(stack_id)
954            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
955
956        info!("Checking Git integrity for stack '{}'", stack.name);
957
958        // Detect all modifications
959        let mut modifications = Vec::new();
960        for entry in &stack.entries {
961            if !self.repo.branch_exists(&entry.branch) {
962                modifications.push(BranchModification::Missing {
963                    branch: entry.branch.clone(),
964                    entry_id: entry.id,
965                    expected_commit: entry.commit_hash.clone(),
966                });
967            } else if let Ok(branch_head) = self.repo.get_branch_head(&entry.branch) {
968                if branch_head != entry.commit_hash {
969                    // Get extra commits and their messages
970                    let extra_commits = self
971                        .repo
972                        .get_commits_between(&entry.commit_hash, &branch_head)?;
973                    let mut extra_messages = Vec::new();
974                    for commit in &extra_commits {
975                        if let Some(message) = commit.message() {
976                            let first_line =
977                                message.lines().next().unwrap_or("(no message)").to_string();
978                            extra_messages.push(format!(
979                                "{}: {}",
980                                &commit.id().to_string()[..8],
981                                first_line
982                            ));
983                        }
984                    }
985
986                    modifications.push(BranchModification::ExtraCommits {
987                        branch: entry.branch.clone(),
988                        entry_id: entry.id,
989                        expected_commit: entry.commit_hash.clone(),
990                        actual_commit: branch_head,
991                        extra_commit_count: extra_commits.len(),
992                        extra_commit_messages: extra_messages,
993                    });
994                }
995            }
996        }
997
998        if modifications.is_empty() {
999            Output::success(format!(
1000                "Stack '{}' has no Git integrity issues",
1001                stack.name
1002            ));
1003            return Ok(());
1004        }
1005
1006        // Show detected modifications
1007        Output::check_start(format!(
1008            "Detected branch modifications in stack '{}':",
1009            stack.name
1010        ));
1011        for (i, modification) in modifications.iter().enumerate() {
1012            match modification {
1013                BranchModification::Missing { branch, .. } => {
1014                    Output::numbered_item(i + 1, format!("Branch '{branch}' is missing"));
1015                }
1016                BranchModification::ExtraCommits {
1017                    branch,
1018                    expected_commit,
1019                    actual_commit,
1020                    extra_commit_count,
1021                    extra_commit_messages,
1022                    ..
1023                } => {
1024                    println!(
1025                        "   {}. Branch '{}' has {} extra commit(s)",
1026                        i + 1,
1027                        branch,
1028                        extra_commit_count
1029                    );
1030                    println!(
1031                        "      Expected: {} | Actual: {}",
1032                        &expected_commit[..8],
1033                        &actual_commit[..8]
1034                    );
1035
1036                    // Show extra commit messages (first few only)
1037                    for (j, message) in extra_commit_messages.iter().enumerate() {
1038                        if j < 3 {
1039                            Output::sub_item(format!("     + {message}"));
1040                        } else if j == 3 {
1041                            Output::sub_item(format!(
1042                                "     + ... and {} more",
1043                                extra_commit_count - 3
1044                            ));
1045                            break;
1046                        }
1047                    }
1048                }
1049            }
1050        }
1051        Output::spacing();
1052
1053        // Auto mode handling
1054        if let Some(mode) = auto_mode {
1055            return self.apply_auto_fix(stack_id, &modifications, &mode);
1056        }
1057
1058        // Interactive mode - ask user for each modification
1059        for modification in modifications {
1060            self.handle_single_modification(stack_id, &modification)?;
1061        }
1062
1063        self.save_to_disk()?;
1064        Output::success("All branch modifications handled successfully!");
1065        Ok(())
1066    }
1067
1068    /// Handle a single branch modification interactively
1069    fn handle_single_modification(
1070        &mut self,
1071        stack_id: &Uuid,
1072        modification: &BranchModification,
1073    ) -> Result<()> {
1074        match modification {
1075            BranchModification::Missing {
1076                branch,
1077                expected_commit,
1078                ..
1079            } => {
1080                Output::info(format!("šŸ”§ Missing branch '{branch}'"));
1081                Output::sub_item(format!(
1082                    "This will create the branch at commit {}",
1083                    &expected_commit[..8]
1084                ));
1085
1086                self.repo.create_branch(branch, Some(expected_commit))?;
1087                Output::success(format!("Created branch '{branch}'"));
1088            }
1089
1090            BranchModification::ExtraCommits {
1091                branch,
1092                entry_id,
1093                expected_commit,
1094                extra_commit_count,
1095                ..
1096            } => {
1097                Output::info(format!(
1098                    "šŸ¤” Branch '{branch}' has {extra_commit_count} extra commit(s). What would you like to do?"
1099                ));
1100                let options = vec![
1101                    "šŸ“ Incorporate - Update stack entry to include extra commits",
1102                    "āž• Split - Create new stack entry for extra commits",
1103                    "šŸ—‘ļø  Reset - Remove extra commits (DESTRUCTIVE)",
1104                    "ā­ļø  Skip - Leave as-is for now",
1105                ];
1106
1107                let choice = Select::with_theme(&ColorfulTheme::default())
1108                    .with_prompt("Choose how to handle extra commits")
1109                    .default(0)
1110                    .items(&options)
1111                    .interact()
1112                    .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
1113
1114                match choice {
1115                    0 => {
1116                        self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1117                    }
1118                    1 => {
1119                        self.split_extra_commits(stack_id, *entry_id, branch)?;
1120                    }
1121                    2 => {
1122                        self.reset_branch_destructive(branch, expected_commit)?;
1123                    }
1124                    3 => {
1125                        Output::sub_item(format!(
1126                            "ā­ļø  Skipping '{branch}' (integrity issue remains)"
1127                        ));
1128                    }
1129                    _ => {
1130                        Output::sub_item(format!("āŒ Invalid choice. Skipping '{branch}'"));
1131                    }
1132                }
1133            }
1134        }
1135
1136        Ok(())
1137    }
1138
1139    /// Apply automatic fix based on mode
1140    fn apply_auto_fix(
1141        &mut self,
1142        stack_id: &Uuid,
1143        modifications: &[BranchModification],
1144        mode: &str,
1145    ) -> Result<()> {
1146        Output::info(format!("šŸ¤– Applying automatic fix mode: {mode}"));
1147
1148        for modification in modifications {
1149            match (modification, mode) {
1150                (
1151                    BranchModification::Missing {
1152                        branch,
1153                        expected_commit,
1154                        ..
1155                    },
1156                    _,
1157                ) => {
1158                    self.repo.create_branch(branch, Some(expected_commit))?;
1159                    Output::success(format!("Created missing branch '{branch}'"));
1160                }
1161
1162                (
1163                    BranchModification::ExtraCommits {
1164                        branch, entry_id, ..
1165                    },
1166                    "incorporate",
1167                ) => {
1168                    self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1169                }
1170
1171                (
1172                    BranchModification::ExtraCommits {
1173                        branch, entry_id, ..
1174                    },
1175                    "split",
1176                ) => {
1177                    self.split_extra_commits(stack_id, *entry_id, branch)?;
1178                }
1179
1180                (
1181                    BranchModification::ExtraCommits {
1182                        branch,
1183                        expected_commit,
1184                        ..
1185                    },
1186                    "reset",
1187                ) => {
1188                    self.reset_branch_destructive(branch, expected_commit)?;
1189                }
1190
1191                _ => {
1192                    return Err(CascadeError::config(format!(
1193                        "Unknown auto-fix mode '{mode}'. Use: incorporate, split, reset"
1194                    )));
1195                }
1196            }
1197        }
1198
1199        self.save_to_disk()?;
1200        Output::success(format!("Auto-fix completed for mode: {mode}"));
1201        Ok(())
1202    }
1203
1204    /// Incorporate extra commits into the existing stack entry (update commit hash)
1205    fn incorporate_extra_commits(
1206        &mut self,
1207        stack_id: &Uuid,
1208        entry_id: Uuid,
1209        branch: &str,
1210    ) -> Result<()> {
1211        let stack = self.stacks.get_mut(stack_id).unwrap();
1212
1213        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
1214            let new_head = self.repo.get_branch_head(branch)?;
1215            let old_commit = entry.commit_hash[..8].to_string(); // Clone to avoid borrowing issue
1216
1217            // Get the extra commits for message update
1218            let extra_commits = self
1219                .repo
1220                .get_commits_between(&entry.commit_hash, &new_head)?;
1221
1222            // Update the entry to point to the new HEAD
1223            entry.commit_hash = new_head.clone();
1224
1225            // Update commit message to reflect the incorporation
1226            let mut extra_messages = Vec::new();
1227            for commit in &extra_commits {
1228                if let Some(message) = commit.message() {
1229                    let first_line = message.lines().next().unwrap_or("").to_string();
1230                    extra_messages.push(first_line);
1231                }
1232            }
1233
1234            if !extra_messages.is_empty() {
1235                entry.message = format!(
1236                    "{}\n\nIncorporated commits:\n• {}",
1237                    entry.message,
1238                    extra_messages.join("\n• ")
1239                );
1240            }
1241
1242            Output::success(format!(
1243                "Incorporated {} commit(s) into entry '{}'",
1244                extra_commits.len(),
1245                entry.short_hash()
1246            ));
1247            Output::sub_item(format!("Updated: {} -> {}", old_commit, &new_head[..8]));
1248        }
1249
1250        Ok(())
1251    }
1252
1253    /// Split extra commits into a new stack entry
1254    fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1255        let stack = self.stacks.get_mut(stack_id).unwrap();
1256        let new_head = self.repo.get_branch_head(branch)?;
1257
1258        // Find the position of the current entry
1259        let entry_position = stack
1260            .entries
1261            .iter()
1262            .position(|e| e.id == entry_id)
1263            .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1264
1265        // Create a new branch name for the split
1266        let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1267        let new_branch = format!("{base_name}-continued");
1268
1269        // Create new branch at the current HEAD
1270        self.repo.create_branch(&new_branch, Some(&new_head))?;
1271
1272        // Get extra commits for message creation
1273        let original_entry = &stack.entries[entry_position];
1274        let original_commit_hash = original_entry.commit_hash.clone(); // Clone to avoid borrowing issue
1275        let extra_commits = self
1276            .repo
1277            .get_commits_between(&original_commit_hash, &new_head)?;
1278
1279        // Create commit message from extra commits
1280        let mut extra_messages = Vec::new();
1281        for commit in &extra_commits {
1282            if let Some(message) = commit.message() {
1283                let first_line = message.lines().next().unwrap_or("").to_string();
1284                extra_messages.push(first_line);
1285            }
1286        }
1287
1288        let new_message = if extra_messages.len() == 1 {
1289            extra_messages[0].clone()
1290        } else {
1291            format!("Combined changes:\n• {}", extra_messages.join("\n• "))
1292        };
1293
1294        // Create new stack entry manually (no constructor method exists)
1295        let now = chrono::Utc::now();
1296        let new_entry = crate::stack::StackEntry {
1297            id: uuid::Uuid::new_v4(),
1298            branch: new_branch.clone(),
1299            commit_hash: new_head,
1300            message: new_message,
1301            parent_id: Some(entry_id), // Parent is the current entry
1302            children: Vec::new(),
1303            created_at: now,
1304            updated_at: now,
1305            is_submitted: false,
1306            pull_request_id: None,
1307            is_synced: false,
1308        };
1309
1310        // Insert the new entry after the current one
1311        stack.entries.insert(entry_position + 1, new_entry);
1312
1313        // Reset the original branch to its expected commit
1314        self.repo
1315            .reset_branch_to_commit(branch, &original_commit_hash)?;
1316
1317        println!(
1318            "   āœ… Split {} commit(s) into new entry '{}'",
1319            extra_commits.len(),
1320            new_branch
1321        );
1322        println!("      Original branch '{branch}' reset to expected commit");
1323
1324        Ok(())
1325    }
1326
1327    /// Reset branch to expected commit (destructive - loses extra work)
1328    fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1329        self.repo.reset_branch_to_commit(branch, expected_commit)?;
1330        Output::warning(format!(
1331            "Reset branch '{}' to {} (extra commits lost)",
1332            branch,
1333            &expected_commit[..8]
1334        ));
1335        Ok(())
1336    }
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342    use std::process::Command;
1343    use tempfile::TempDir;
1344
1345    fn create_test_repo() -> (TempDir, PathBuf) {
1346        let temp_dir = TempDir::new().unwrap();
1347        let repo_path = temp_dir.path().to_path_buf();
1348
1349        // Initialize git repository
1350        Command::new("git")
1351            .args(["init"])
1352            .current_dir(&repo_path)
1353            .output()
1354            .unwrap();
1355
1356        // Configure git
1357        Command::new("git")
1358            .args(["config", "user.name", "Test User"])
1359            .current_dir(&repo_path)
1360            .output()
1361            .unwrap();
1362
1363        Command::new("git")
1364            .args(["config", "user.email", "test@example.com"])
1365            .current_dir(&repo_path)
1366            .output()
1367            .unwrap();
1368
1369        // Create an initial commit
1370        std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1371        Command::new("git")
1372            .args(["add", "."])
1373            .current_dir(&repo_path)
1374            .output()
1375            .unwrap();
1376
1377        Command::new("git")
1378            .args(["commit", "-m", "Initial commit"])
1379            .current_dir(&repo_path)
1380            .output()
1381            .unwrap();
1382
1383        // Initialize cascade
1384        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1385            .unwrap();
1386
1387        (temp_dir, repo_path)
1388    }
1389
1390    #[test]
1391    fn test_create_stack_manager() {
1392        let (_temp_dir, repo_path) = create_test_repo();
1393        let manager = StackManager::new(&repo_path).unwrap();
1394
1395        assert_eq!(manager.stacks.len(), 0);
1396        assert!(manager.get_active_stack().is_none());
1397    }
1398
1399    #[test]
1400    fn test_create_and_manage_stack() {
1401        let (_temp_dir, repo_path) = create_test_repo();
1402        let mut manager = StackManager::new(&repo_path).unwrap();
1403
1404        // Create a stack using the default branch
1405        let stack_id = manager
1406            .create_stack(
1407                "test-stack".to_string(),
1408                None, // Use default branch
1409                Some("Test stack description".to_string()),
1410            )
1411            .unwrap();
1412
1413        // Verify stack was created
1414        assert_eq!(manager.stacks.len(), 1);
1415        let stack = manager.get_stack(&stack_id).unwrap();
1416        assert_eq!(stack.name, "test-stack");
1417        // Should use the default branch (which gets set from the Git repo)
1418        assert!(!stack.base_branch.is_empty());
1419        assert!(stack.is_active);
1420
1421        // Verify it's the active stack
1422        let active = manager.get_active_stack().unwrap();
1423        assert_eq!(active.id, stack_id);
1424
1425        // Test get by name
1426        let found = manager.get_stack_by_name("test-stack").unwrap();
1427        assert_eq!(found.id, stack_id);
1428    }
1429
1430    #[test]
1431    fn test_stack_persistence() {
1432        let (_temp_dir, repo_path) = create_test_repo();
1433
1434        let stack_id = {
1435            let mut manager = StackManager::new(&repo_path).unwrap();
1436            manager
1437                .create_stack("persistent-stack".to_string(), None, None)
1438                .unwrap()
1439        };
1440
1441        // Create new manager and verify data was loaded
1442        let manager = StackManager::new(&repo_path).unwrap();
1443        assert_eq!(manager.stacks.len(), 1);
1444        let stack = manager.get_stack(&stack_id).unwrap();
1445        assert_eq!(stack.name, "persistent-stack");
1446    }
1447
1448    #[test]
1449    fn test_multiple_stacks() {
1450        let (_temp_dir, repo_path) = create_test_repo();
1451        let mut manager = StackManager::new(&repo_path).unwrap();
1452
1453        let stack1_id = manager
1454            .create_stack("stack-1".to_string(), None, None)
1455            .unwrap();
1456        let stack2_id = manager
1457            .create_stack("stack-2".to_string(), None, None)
1458            .unwrap();
1459
1460        assert_eq!(manager.stacks.len(), 2);
1461
1462        // Second stack should be active (newly created stacks become active)
1463        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1464        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1465
1466        // Change active stack
1467        manager.set_active_stack(Some(stack2_id)).unwrap();
1468        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1469        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1470    }
1471
1472    #[test]
1473    fn test_delete_stack() {
1474        let (_temp_dir, repo_path) = create_test_repo();
1475        let mut manager = StackManager::new(&repo_path).unwrap();
1476
1477        let stack_id = manager
1478            .create_stack("to-delete".to_string(), None, None)
1479            .unwrap();
1480        assert_eq!(manager.stacks.len(), 1);
1481
1482        let deleted = manager.delete_stack(&stack_id).unwrap();
1483        assert_eq!(deleted.name, "to-delete");
1484        assert_eq!(manager.stacks.len(), 0);
1485        assert!(manager.get_active_stack().is_none());
1486    }
1487
1488    #[test]
1489    fn test_validation() {
1490        let (_temp_dir, repo_path) = create_test_repo();
1491        let mut manager = StackManager::new(&repo_path).unwrap();
1492
1493        manager
1494            .create_stack("valid-stack".to_string(), None, None)
1495            .unwrap();
1496
1497        // Should pass validation
1498        assert!(manager.validate_all().is_ok());
1499    }
1500
1501    #[test]
1502    fn test_duplicate_commit_message_detection() {
1503        let (_temp_dir, repo_path) = create_test_repo();
1504        let mut manager = StackManager::new(&repo_path).unwrap();
1505
1506        // Create a stack
1507        manager
1508            .create_stack("test-stack".to_string(), None, None)
1509            .unwrap();
1510
1511        // Create first commit
1512        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1513        Command::new("git")
1514            .args(["add", "file1.txt"])
1515            .current_dir(&repo_path)
1516            .output()
1517            .unwrap();
1518
1519        Command::new("git")
1520            .args(["commit", "-m", "Add authentication feature"])
1521            .current_dir(&repo_path)
1522            .output()
1523            .unwrap();
1524
1525        let commit1_hash = Command::new("git")
1526            .args(["rev-parse", "HEAD"])
1527            .current_dir(&repo_path)
1528            .output()
1529            .unwrap();
1530        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1531            .trim()
1532            .to_string();
1533
1534        // Push first commit to stack - should succeed
1535        let entry1_id = manager
1536            .push_to_stack(
1537                "feature/auth".to_string(),
1538                commit1_hash,
1539                "Add authentication feature".to_string(),
1540                "main".to_string(),
1541            )
1542            .unwrap();
1543
1544        // Verify first entry was added
1545        assert!(manager
1546            .get_active_stack()
1547            .unwrap()
1548            .get_entry(&entry1_id)
1549            .is_some());
1550
1551        // Create second commit
1552        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1553        Command::new("git")
1554            .args(["add", "file2.txt"])
1555            .current_dir(&repo_path)
1556            .output()
1557            .unwrap();
1558
1559        Command::new("git")
1560            .args(["commit", "-m", "Different commit message"])
1561            .current_dir(&repo_path)
1562            .output()
1563            .unwrap();
1564
1565        let commit2_hash = Command::new("git")
1566            .args(["rev-parse", "HEAD"])
1567            .current_dir(&repo_path)
1568            .output()
1569            .unwrap();
1570        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1571            .trim()
1572            .to_string();
1573
1574        // Try to push second commit with the SAME message - should fail
1575        let result = manager.push_to_stack(
1576            "feature/auth2".to_string(),
1577            commit2_hash.clone(),
1578            "Add authentication feature".to_string(), // Same message as first commit
1579            "main".to_string(),
1580        );
1581
1582        // Should fail with validation error
1583        assert!(result.is_err());
1584        let error = result.unwrap_err();
1585        assert!(matches!(error, CascadeError::Validation(_)));
1586
1587        // Error message should contain helpful information
1588        let error_msg = error.to_string();
1589        assert!(error_msg.contains("Duplicate commit message"));
1590        assert!(error_msg.contains("Add authentication feature"));
1591        assert!(error_msg.contains("šŸ’” Consider using a more specific message"));
1592
1593        // Push with different message - should succeed
1594        let entry2_id = manager
1595            .push_to_stack(
1596                "feature/auth2".to_string(),
1597                commit2_hash,
1598                "Add authentication validation".to_string(), // Different message
1599                "main".to_string(),
1600            )
1601            .unwrap();
1602
1603        // Verify both entries exist
1604        let stack = manager.get_active_stack().unwrap();
1605        assert_eq!(stack.entries.len(), 2);
1606        assert!(stack.get_entry(&entry1_id).is_some());
1607        assert!(stack.get_entry(&entry2_id).is_some());
1608    }
1609
1610    #[test]
1611    fn test_duplicate_message_with_different_case() {
1612        let (_temp_dir, repo_path) = create_test_repo();
1613        let mut manager = StackManager::new(&repo_path).unwrap();
1614
1615        manager
1616            .create_stack("test-stack".to_string(), None, None)
1617            .unwrap();
1618
1619        // Create and push first commit
1620        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1621        Command::new("git")
1622            .args(["add", "file1.txt"])
1623            .current_dir(&repo_path)
1624            .output()
1625            .unwrap();
1626
1627        Command::new("git")
1628            .args(["commit", "-m", "fix bug"])
1629            .current_dir(&repo_path)
1630            .output()
1631            .unwrap();
1632
1633        let commit1_hash = Command::new("git")
1634            .args(["rev-parse", "HEAD"])
1635            .current_dir(&repo_path)
1636            .output()
1637            .unwrap();
1638        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1639            .trim()
1640            .to_string();
1641
1642        manager
1643            .push_to_stack(
1644                "feature/fix1".to_string(),
1645                commit1_hash,
1646                "fix bug".to_string(),
1647                "main".to_string(),
1648            )
1649            .unwrap();
1650
1651        // Create second commit
1652        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1653        Command::new("git")
1654            .args(["add", "file2.txt"])
1655            .current_dir(&repo_path)
1656            .output()
1657            .unwrap();
1658
1659        Command::new("git")
1660            .args(["commit", "-m", "Fix Bug"])
1661            .current_dir(&repo_path)
1662            .output()
1663            .unwrap();
1664
1665        let commit2_hash = Command::new("git")
1666            .args(["rev-parse", "HEAD"])
1667            .current_dir(&repo_path)
1668            .output()
1669            .unwrap();
1670        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1671            .trim()
1672            .to_string();
1673
1674        // Different case should be allowed (case-sensitive comparison)
1675        let result = manager.push_to_stack(
1676            "feature/fix2".to_string(),
1677            commit2_hash,
1678            "Fix Bug".to_string(), // Different case
1679            "main".to_string(),
1680        );
1681
1682        // Should succeed because it's case-sensitive
1683        assert!(result.is_ok());
1684    }
1685
1686    #[test]
1687    fn test_duplicate_message_across_different_stacks() {
1688        let (_temp_dir, repo_path) = create_test_repo();
1689        let mut manager = StackManager::new(&repo_path).unwrap();
1690
1691        // Create first stack and push commit
1692        let stack1_id = manager
1693            .create_stack("stack1".to_string(), None, None)
1694            .unwrap();
1695
1696        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1697        Command::new("git")
1698            .args(["add", "file1.txt"])
1699            .current_dir(&repo_path)
1700            .output()
1701            .unwrap();
1702
1703        Command::new("git")
1704            .args(["commit", "-m", "shared message"])
1705            .current_dir(&repo_path)
1706            .output()
1707            .unwrap();
1708
1709        let commit1_hash = Command::new("git")
1710            .args(["rev-parse", "HEAD"])
1711            .current_dir(&repo_path)
1712            .output()
1713            .unwrap();
1714        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1715            .trim()
1716            .to_string();
1717
1718        manager
1719            .push_to_stack(
1720                "feature/shared1".to_string(),
1721                commit1_hash,
1722                "shared message".to_string(),
1723                "main".to_string(),
1724            )
1725            .unwrap();
1726
1727        // Create second stack
1728        let stack2_id = manager
1729            .create_stack("stack2".to_string(), None, None)
1730            .unwrap();
1731
1732        // Set second stack as active
1733        manager.set_active_stack(Some(stack2_id)).unwrap();
1734
1735        // Create commit for second stack
1736        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1737        Command::new("git")
1738            .args(["add", "file2.txt"])
1739            .current_dir(&repo_path)
1740            .output()
1741            .unwrap();
1742
1743        Command::new("git")
1744            .args(["commit", "-m", "shared message"])
1745            .current_dir(&repo_path)
1746            .output()
1747            .unwrap();
1748
1749        let commit2_hash = Command::new("git")
1750            .args(["rev-parse", "HEAD"])
1751            .current_dir(&repo_path)
1752            .output()
1753            .unwrap();
1754        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1755            .trim()
1756            .to_string();
1757
1758        // Same message in different stack should be allowed
1759        let result = manager.push_to_stack(
1760            "feature/shared2".to_string(),
1761            commit2_hash,
1762            "shared message".to_string(), // Same message but different stack
1763            "main".to_string(),
1764        );
1765
1766        // Should succeed because it's a different stack
1767        assert!(result.is_ok());
1768
1769        // Verify both stacks have entries with the same message
1770        let stack1 = manager.get_stack(&stack1_id).unwrap();
1771        let stack2 = manager.get_stack(&stack2_id).unwrap();
1772
1773        assert_eq!(stack1.entries.len(), 1);
1774        assert_eq!(stack2.entries.len(), 1);
1775        assert_eq!(stack1.entries[0].message, "shared message");
1776        assert_eq!(stack2.entries[0].message, "shared message");
1777    }
1778
1779    #[test]
1780    fn test_duplicate_after_pop() {
1781        let (_temp_dir, repo_path) = create_test_repo();
1782        let mut manager = StackManager::new(&repo_path).unwrap();
1783
1784        manager
1785            .create_stack("test-stack".to_string(), None, None)
1786            .unwrap();
1787
1788        // Create and push first commit
1789        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1790        Command::new("git")
1791            .args(["add", "file1.txt"])
1792            .current_dir(&repo_path)
1793            .output()
1794            .unwrap();
1795
1796        Command::new("git")
1797            .args(["commit", "-m", "temporary message"])
1798            .current_dir(&repo_path)
1799            .output()
1800            .unwrap();
1801
1802        let commit1_hash = Command::new("git")
1803            .args(["rev-parse", "HEAD"])
1804            .current_dir(&repo_path)
1805            .output()
1806            .unwrap();
1807        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1808            .trim()
1809            .to_string();
1810
1811        manager
1812            .push_to_stack(
1813                "feature/temp".to_string(),
1814                commit1_hash,
1815                "temporary message".to_string(),
1816                "main".to_string(),
1817            )
1818            .unwrap();
1819
1820        // Pop the entry
1821        let popped = manager.pop_from_stack().unwrap();
1822        assert_eq!(popped.message, "temporary message");
1823
1824        // Create new commit
1825        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1826        Command::new("git")
1827            .args(["add", "file2.txt"])
1828            .current_dir(&repo_path)
1829            .output()
1830            .unwrap();
1831
1832        Command::new("git")
1833            .args(["commit", "-m", "temporary message"])
1834            .current_dir(&repo_path)
1835            .output()
1836            .unwrap();
1837
1838        let commit2_hash = Command::new("git")
1839            .args(["rev-parse", "HEAD"])
1840            .current_dir(&repo_path)
1841            .output()
1842            .unwrap();
1843        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1844            .trim()
1845            .to_string();
1846
1847        // Should be able to push same message again after popping
1848        let result = manager.push_to_stack(
1849            "feature/temp2".to_string(),
1850            commit2_hash,
1851            "temporary message".to_string(),
1852            "main".to_string(),
1853        );
1854
1855        assert!(result.is_ok());
1856    }
1857}