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.stacks.get_mut(stack_id).unwrap();
1226
1227        if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
1228            let new_head = self.repo.get_branch_head(branch)?;
1229            let old_commit = entry.commit_hash[..8].to_string(); // Clone to avoid borrowing issue
1230
1231            // Get the extra commits for message update
1232            let extra_commits = self
1233                .repo
1234                .get_commits_between(&entry.commit_hash, &new_head)?;
1235
1236            // Update the entry to point to the new HEAD
1237            entry.commit_hash = new_head.clone();
1238
1239            // Update commit message to reflect the incorporation
1240            let mut extra_messages = Vec::new();
1241            for commit in &extra_commits {
1242                if let Some(message) = commit.message() {
1243                    let first_line = message.lines().next().unwrap_or("").to_string();
1244                    extra_messages.push(first_line);
1245                }
1246            }
1247
1248            if !extra_messages.is_empty() {
1249                entry.message = format!(
1250                    "{}\n\nIncorporated commits:\n• {}",
1251                    entry.message,
1252                    extra_messages.join("\n• ")
1253                );
1254            }
1255
1256            Output::success(format!(
1257                "Incorporated {} commit(s) into entry '{}'",
1258                extra_commits.len(),
1259                entry.short_hash()
1260            ));
1261            Output::sub_item(format!("Updated: {} -> {}", old_commit, &new_head[..8]));
1262        }
1263
1264        Ok(())
1265    }
1266
1267    /// Split extra commits into a new stack entry
1268    fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1269        let stack = self.stacks.get_mut(stack_id).unwrap();
1270        let new_head = self.repo.get_branch_head(branch)?;
1271
1272        // Find the position of the current entry
1273        let entry_position = stack
1274            .entries
1275            .iter()
1276            .position(|e| e.id == entry_id)
1277            .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1278
1279        // Create a new branch name for the split
1280        let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1281        let new_branch = format!("{base_name}-continued");
1282
1283        // Create new branch at the current HEAD
1284        self.repo.create_branch(&new_branch, Some(&new_head))?;
1285
1286        // Get extra commits for message creation
1287        let original_entry = &stack.entries[entry_position];
1288        let original_commit_hash = original_entry.commit_hash.clone(); // Clone to avoid borrowing issue
1289        let extra_commits = self
1290            .repo
1291            .get_commits_between(&original_commit_hash, &new_head)?;
1292
1293        // Create commit message from extra commits
1294        let mut extra_messages = Vec::new();
1295        for commit in &extra_commits {
1296            if let Some(message) = commit.message() {
1297                let first_line = message.lines().next().unwrap_or("").to_string();
1298                extra_messages.push(first_line);
1299            }
1300        }
1301
1302        let new_message = if extra_messages.len() == 1 {
1303            extra_messages[0].clone()
1304        } else {
1305            format!("Combined changes:\n• {}", extra_messages.join("\n• "))
1306        };
1307
1308        // Create new stack entry manually (no constructor method exists)
1309        let now = chrono::Utc::now();
1310        let new_entry = crate::stack::StackEntry {
1311            id: uuid::Uuid::new_v4(),
1312            branch: new_branch.clone(),
1313            commit_hash: new_head,
1314            message: new_message,
1315            parent_id: Some(entry_id), // Parent is the current entry
1316            children: Vec::new(),
1317            created_at: now,
1318            updated_at: now,
1319            is_submitted: false,
1320            pull_request_id: None,
1321            is_synced: false,
1322        };
1323
1324        // Insert the new entry after the current one
1325        stack.entries.insert(entry_position + 1, new_entry);
1326
1327        // Reset the original branch to its expected commit
1328        self.repo
1329            .reset_branch_to_commit(branch, &original_commit_hash)?;
1330
1331        println!(
1332            "   āœ… Split {} commit(s) into new entry '{}'",
1333            extra_commits.len(),
1334            new_branch
1335        );
1336        println!("      Original branch '{branch}' reset to expected commit");
1337
1338        Ok(())
1339    }
1340
1341    /// Reset branch to expected commit (destructive - loses extra work)
1342    fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1343        self.repo.reset_branch_to_commit(branch, expected_commit)?;
1344        Output::warning(format!(
1345            "Reset branch '{}' to {} (extra commits lost)",
1346            branch,
1347            &expected_commit[..8]
1348        ));
1349        Ok(())
1350    }
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355    use super::*;
1356    use std::process::Command;
1357    use tempfile::TempDir;
1358
1359    fn create_test_repo() -> (TempDir, PathBuf) {
1360        let temp_dir = TempDir::new().unwrap();
1361        let repo_path = temp_dir.path().to_path_buf();
1362
1363        // Initialize git repository
1364        Command::new("git")
1365            .args(["init"])
1366            .current_dir(&repo_path)
1367            .output()
1368            .unwrap();
1369
1370        // Configure git
1371        Command::new("git")
1372            .args(["config", "user.name", "Test User"])
1373            .current_dir(&repo_path)
1374            .output()
1375            .unwrap();
1376
1377        Command::new("git")
1378            .args(["config", "user.email", "test@example.com"])
1379            .current_dir(&repo_path)
1380            .output()
1381            .unwrap();
1382
1383        // Create an initial commit
1384        std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1385        Command::new("git")
1386            .args(["add", "."])
1387            .current_dir(&repo_path)
1388            .output()
1389            .unwrap();
1390
1391        Command::new("git")
1392            .args(["commit", "-m", "Initial commit"])
1393            .current_dir(&repo_path)
1394            .output()
1395            .unwrap();
1396
1397        // Initialize cascade
1398        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1399            .unwrap();
1400
1401        (temp_dir, repo_path)
1402    }
1403
1404    #[test]
1405    fn test_create_stack_manager() {
1406        let (_temp_dir, repo_path) = create_test_repo();
1407        let manager = StackManager::new(&repo_path).unwrap();
1408
1409        assert_eq!(manager.stacks.len(), 0);
1410        assert!(manager.get_active_stack().is_none());
1411    }
1412
1413    #[test]
1414    fn test_create_and_manage_stack() {
1415        let (_temp_dir, repo_path) = create_test_repo();
1416        let mut manager = StackManager::new(&repo_path).unwrap();
1417
1418        // Create a stack using the default branch
1419        let stack_id = manager
1420            .create_stack(
1421                "test-stack".to_string(),
1422                None, // Use default branch
1423                Some("Test stack description".to_string()),
1424            )
1425            .unwrap();
1426
1427        // Verify stack was created
1428        assert_eq!(manager.stacks.len(), 1);
1429        let stack = manager.get_stack(&stack_id).unwrap();
1430        assert_eq!(stack.name, "test-stack");
1431        // Should use the default branch (which gets set from the Git repo)
1432        assert!(!stack.base_branch.is_empty());
1433        assert!(stack.is_active);
1434
1435        // Verify it's the active stack
1436        let active = manager.get_active_stack().unwrap();
1437        assert_eq!(active.id, stack_id);
1438
1439        // Test get by name
1440        let found = manager.get_stack_by_name("test-stack").unwrap();
1441        assert_eq!(found.id, stack_id);
1442    }
1443
1444    #[test]
1445    fn test_stack_persistence() {
1446        let (_temp_dir, repo_path) = create_test_repo();
1447
1448        let stack_id = {
1449            let mut manager = StackManager::new(&repo_path).unwrap();
1450            manager
1451                .create_stack("persistent-stack".to_string(), None, None)
1452                .unwrap()
1453        };
1454
1455        // Create new manager and verify data was loaded
1456        let manager = StackManager::new(&repo_path).unwrap();
1457        assert_eq!(manager.stacks.len(), 1);
1458        let stack = manager.get_stack(&stack_id).unwrap();
1459        assert_eq!(stack.name, "persistent-stack");
1460    }
1461
1462    #[test]
1463    fn test_multiple_stacks() {
1464        let (_temp_dir, repo_path) = create_test_repo();
1465        let mut manager = StackManager::new(&repo_path).unwrap();
1466
1467        let stack1_id = manager
1468            .create_stack("stack-1".to_string(), None, None)
1469            .unwrap();
1470        let stack2_id = manager
1471            .create_stack("stack-2".to_string(), None, None)
1472            .unwrap();
1473
1474        assert_eq!(manager.stacks.len(), 2);
1475
1476        // Second stack should be active (newly created stacks become active)
1477        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1478        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1479
1480        // Change active stack
1481        manager.set_active_stack(Some(stack2_id)).unwrap();
1482        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1483        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1484    }
1485
1486    #[test]
1487    fn test_delete_stack() {
1488        let (_temp_dir, repo_path) = create_test_repo();
1489        let mut manager = StackManager::new(&repo_path).unwrap();
1490
1491        let stack_id = manager
1492            .create_stack("to-delete".to_string(), None, None)
1493            .unwrap();
1494        assert_eq!(manager.stacks.len(), 1);
1495
1496        let deleted = manager.delete_stack(&stack_id).unwrap();
1497        assert_eq!(deleted.name, "to-delete");
1498        assert_eq!(manager.stacks.len(), 0);
1499        assert!(manager.get_active_stack().is_none());
1500    }
1501
1502    #[test]
1503    fn test_validation() {
1504        let (_temp_dir, repo_path) = create_test_repo();
1505        let mut manager = StackManager::new(&repo_path).unwrap();
1506
1507        manager
1508            .create_stack("valid-stack".to_string(), None, None)
1509            .unwrap();
1510
1511        // Should pass validation
1512        assert!(manager.validate_all().is_ok());
1513    }
1514
1515    #[test]
1516    fn test_duplicate_commit_message_detection() {
1517        let (_temp_dir, repo_path) = create_test_repo();
1518        let mut manager = StackManager::new(&repo_path).unwrap();
1519
1520        // Create a stack
1521        manager
1522            .create_stack("test-stack".to_string(), None, None)
1523            .unwrap();
1524
1525        // Create first commit
1526        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1527        Command::new("git")
1528            .args(["add", "file1.txt"])
1529            .current_dir(&repo_path)
1530            .output()
1531            .unwrap();
1532
1533        Command::new("git")
1534            .args(["commit", "-m", "Add authentication feature"])
1535            .current_dir(&repo_path)
1536            .output()
1537            .unwrap();
1538
1539        let commit1_hash = Command::new("git")
1540            .args(["rev-parse", "HEAD"])
1541            .current_dir(&repo_path)
1542            .output()
1543            .unwrap();
1544        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1545            .trim()
1546            .to_string();
1547
1548        // Push first commit to stack - should succeed
1549        let entry1_id = manager
1550            .push_to_stack(
1551                "feature/auth".to_string(),
1552                commit1_hash,
1553                "Add authentication feature".to_string(),
1554                "main".to_string(),
1555            )
1556            .unwrap();
1557
1558        // Verify first entry was added
1559        assert!(manager
1560            .get_active_stack()
1561            .unwrap()
1562            .get_entry(&entry1_id)
1563            .is_some());
1564
1565        // Create second commit
1566        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1567        Command::new("git")
1568            .args(["add", "file2.txt"])
1569            .current_dir(&repo_path)
1570            .output()
1571            .unwrap();
1572
1573        Command::new("git")
1574            .args(["commit", "-m", "Different commit message"])
1575            .current_dir(&repo_path)
1576            .output()
1577            .unwrap();
1578
1579        let commit2_hash = Command::new("git")
1580            .args(["rev-parse", "HEAD"])
1581            .current_dir(&repo_path)
1582            .output()
1583            .unwrap();
1584        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1585            .trim()
1586            .to_string();
1587
1588        // Try to push second commit with the SAME message - should fail
1589        let result = manager.push_to_stack(
1590            "feature/auth2".to_string(),
1591            commit2_hash.clone(),
1592            "Add authentication feature".to_string(), // Same message as first commit
1593            "main".to_string(),
1594        );
1595
1596        // Should fail with validation error
1597        assert!(result.is_err());
1598        let error = result.unwrap_err();
1599        assert!(matches!(error, CascadeError::Validation(_)));
1600
1601        // Error message should contain helpful information
1602        let error_msg = error.to_string();
1603        assert!(error_msg.contains("Duplicate commit message"));
1604        assert!(error_msg.contains("Add authentication feature"));
1605        assert!(error_msg.contains("šŸ’” Consider using a more specific message"));
1606
1607        // Push with different message - should succeed
1608        let entry2_id = manager
1609            .push_to_stack(
1610                "feature/auth2".to_string(),
1611                commit2_hash,
1612                "Add authentication validation".to_string(), // Different message
1613                "main".to_string(),
1614            )
1615            .unwrap();
1616
1617        // Verify both entries exist
1618        let stack = manager.get_active_stack().unwrap();
1619        assert_eq!(stack.entries.len(), 2);
1620        assert!(stack.get_entry(&entry1_id).is_some());
1621        assert!(stack.get_entry(&entry2_id).is_some());
1622    }
1623
1624    #[test]
1625    fn test_duplicate_message_with_different_case() {
1626        let (_temp_dir, repo_path) = create_test_repo();
1627        let mut manager = StackManager::new(&repo_path).unwrap();
1628
1629        manager
1630            .create_stack("test-stack".to_string(), None, None)
1631            .unwrap();
1632
1633        // Create and push first commit
1634        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1635        Command::new("git")
1636            .args(["add", "file1.txt"])
1637            .current_dir(&repo_path)
1638            .output()
1639            .unwrap();
1640
1641        Command::new("git")
1642            .args(["commit", "-m", "fix bug"])
1643            .current_dir(&repo_path)
1644            .output()
1645            .unwrap();
1646
1647        let commit1_hash = Command::new("git")
1648            .args(["rev-parse", "HEAD"])
1649            .current_dir(&repo_path)
1650            .output()
1651            .unwrap();
1652        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1653            .trim()
1654            .to_string();
1655
1656        manager
1657            .push_to_stack(
1658                "feature/fix1".to_string(),
1659                commit1_hash,
1660                "fix bug".to_string(),
1661                "main".to_string(),
1662            )
1663            .unwrap();
1664
1665        // Create second commit
1666        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1667        Command::new("git")
1668            .args(["add", "file2.txt"])
1669            .current_dir(&repo_path)
1670            .output()
1671            .unwrap();
1672
1673        Command::new("git")
1674            .args(["commit", "-m", "Fix Bug"])
1675            .current_dir(&repo_path)
1676            .output()
1677            .unwrap();
1678
1679        let commit2_hash = Command::new("git")
1680            .args(["rev-parse", "HEAD"])
1681            .current_dir(&repo_path)
1682            .output()
1683            .unwrap();
1684        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1685            .trim()
1686            .to_string();
1687
1688        // Different case should be allowed (case-sensitive comparison)
1689        let result = manager.push_to_stack(
1690            "feature/fix2".to_string(),
1691            commit2_hash,
1692            "Fix Bug".to_string(), // Different case
1693            "main".to_string(),
1694        );
1695
1696        // Should succeed because it's case-sensitive
1697        assert!(result.is_ok());
1698    }
1699
1700    #[test]
1701    fn test_duplicate_message_across_different_stacks() {
1702        let (_temp_dir, repo_path) = create_test_repo();
1703        let mut manager = StackManager::new(&repo_path).unwrap();
1704
1705        // Create first stack and push commit
1706        let stack1_id = manager
1707            .create_stack("stack1".to_string(), None, None)
1708            .unwrap();
1709
1710        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1711        Command::new("git")
1712            .args(["add", "file1.txt"])
1713            .current_dir(&repo_path)
1714            .output()
1715            .unwrap();
1716
1717        Command::new("git")
1718            .args(["commit", "-m", "shared message"])
1719            .current_dir(&repo_path)
1720            .output()
1721            .unwrap();
1722
1723        let commit1_hash = Command::new("git")
1724            .args(["rev-parse", "HEAD"])
1725            .current_dir(&repo_path)
1726            .output()
1727            .unwrap();
1728        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1729            .trim()
1730            .to_string();
1731
1732        manager
1733            .push_to_stack(
1734                "feature/shared1".to_string(),
1735                commit1_hash,
1736                "shared message".to_string(),
1737                "main".to_string(),
1738            )
1739            .unwrap();
1740
1741        // Create second stack
1742        let stack2_id = manager
1743            .create_stack("stack2".to_string(), None, None)
1744            .unwrap();
1745
1746        // Set second stack as active
1747        manager.set_active_stack(Some(stack2_id)).unwrap();
1748
1749        // Create commit for second stack
1750        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1751        Command::new("git")
1752            .args(["add", "file2.txt"])
1753            .current_dir(&repo_path)
1754            .output()
1755            .unwrap();
1756
1757        Command::new("git")
1758            .args(["commit", "-m", "shared message"])
1759            .current_dir(&repo_path)
1760            .output()
1761            .unwrap();
1762
1763        let commit2_hash = Command::new("git")
1764            .args(["rev-parse", "HEAD"])
1765            .current_dir(&repo_path)
1766            .output()
1767            .unwrap();
1768        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1769            .trim()
1770            .to_string();
1771
1772        // Same message in different stack should be allowed
1773        let result = manager.push_to_stack(
1774            "feature/shared2".to_string(),
1775            commit2_hash,
1776            "shared message".to_string(), // Same message but different stack
1777            "main".to_string(),
1778        );
1779
1780        // Should succeed because it's a different stack
1781        assert!(result.is_ok());
1782
1783        // Verify both stacks have entries with the same message
1784        let stack1 = manager.get_stack(&stack1_id).unwrap();
1785        let stack2 = manager.get_stack(&stack2_id).unwrap();
1786
1787        assert_eq!(stack1.entries.len(), 1);
1788        assert_eq!(stack2.entries.len(), 1);
1789        assert_eq!(stack1.entries[0].message, "shared message");
1790        assert_eq!(stack2.entries[0].message, "shared message");
1791    }
1792
1793    #[test]
1794    fn test_duplicate_after_pop() {
1795        let (_temp_dir, repo_path) = create_test_repo();
1796        let mut manager = StackManager::new(&repo_path).unwrap();
1797
1798        manager
1799            .create_stack("test-stack".to_string(), None, None)
1800            .unwrap();
1801
1802        // Create and push first commit
1803        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1804        Command::new("git")
1805            .args(["add", "file1.txt"])
1806            .current_dir(&repo_path)
1807            .output()
1808            .unwrap();
1809
1810        Command::new("git")
1811            .args(["commit", "-m", "temporary message"])
1812            .current_dir(&repo_path)
1813            .output()
1814            .unwrap();
1815
1816        let commit1_hash = Command::new("git")
1817            .args(["rev-parse", "HEAD"])
1818            .current_dir(&repo_path)
1819            .output()
1820            .unwrap();
1821        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1822            .trim()
1823            .to_string();
1824
1825        manager
1826            .push_to_stack(
1827                "feature/temp".to_string(),
1828                commit1_hash,
1829                "temporary message".to_string(),
1830                "main".to_string(),
1831            )
1832            .unwrap();
1833
1834        // Pop the entry
1835        let popped = manager.pop_from_stack().unwrap();
1836        assert_eq!(popped.message, "temporary message");
1837
1838        // Create new commit
1839        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1840        Command::new("git")
1841            .args(["add", "file2.txt"])
1842            .current_dir(&repo_path)
1843            .output()
1844            .unwrap();
1845
1846        Command::new("git")
1847            .args(["commit", "-m", "temporary message"])
1848            .current_dir(&repo_path)
1849            .output()
1850            .unwrap();
1851
1852        let commit2_hash = Command::new("git")
1853            .args(["rev-parse", "HEAD"])
1854            .current_dir(&repo_path)
1855            .output()
1856            .unwrap();
1857        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1858            .trim()
1859            .to_string();
1860
1861        // Should be able to push same message again after popping
1862        let result = manager.push_to_stack(
1863            "feature/temp2".to_string(),
1864            commit2_hash,
1865            "temporary message".to_string(),
1866            "main".to_string(),
1867        );
1868
1869        assert!(result.is_ok());
1870    }
1871}