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