use crate::conflict_session::ConflictSession;
use crate::domain::*;
use crate::error::{Error, ErrorKind};
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Clone, Debug, Default)]
pub struct CancellationToken {
    cancelled: Arc<AtomicBool>,
}

impl CancellationToken {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn cancel(&self) {
        self.cancelled.store(true, Ordering::Release);
    }

    pub fn is_cancelled(&self) -> bool {
        self.cancelled.load(Ordering::Acquire)
    }

    pub fn check_cancelled(&self) -> Result<()> {
        if self.is_cancelled() {
            Err(Error::new(ErrorKind::Cancelled))
        } else {
            Ok(())
        }
    }
}

/// A partially built log page, reported while a walk is still running.
///
/// `commits` is the page so far — every chunk is a prefix of the next one and
/// of the final page — and `scanned` counts the commits the walk has visited,
/// matching or not, so a filter that is finding nothing still shows progress.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LogChunk {
    pub commits: Vec<crate::domain::Commit>,
    pub scanned: u64,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CommandOutput {
    pub command: String,
    pub stdout: String,
    pub stderr: String,
    pub exit_code: Option<i32>,
}

impl CommandOutput {
    pub fn empty_success(command: impl Into<String>) -> Self {
        Self {
            command: command.into(),
            stdout: String::new(),
            stderr: String::new(),
            exit_code: Some(0),
        }
    }

    pub fn combined(&self) -> String {
        let mut out = String::new();
        if !self.stdout.trim().is_empty() {
            out.push_str(self.stdout.trim_end());
            out.push('\n');
        }
        if !self.stderr.trim().is_empty() {
            out.push_str(self.stderr.trim_end());
            out.push('\n');
        }
        out.trim_end().to_string()
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConflictSide {
    Ours,
    Theirs,
}

/// Result of launching an external mergetool.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MergetoolResult {
    /// The tool command that was invoked.
    pub tool_name: String,
    /// Whether the tool reported success (exit code 0 or trust-exit-code semantics).
    pub success: bool,
    /// The merged file contents read back after the tool exited, if available.
    pub merged_contents: Option<Vec<u8>>,
    /// Combined stdout/stderr from the tool invocation for diagnostics.
    pub output: CommandOutput,
}

/// Try to decode optional bytes as UTF-8. Returns `None` if the bytes are
/// `None` or not valid UTF-8.
pub fn decode_utf8_optional(bytes: Option<&[u8]>) -> Option<String> {
    bytes.and_then(|b| std::str::from_utf8(b).ok().map(str::to_owned))
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ConflictTextValidation {
    pub has_conflict_markers: bool,
    pub marker_lines: usize,
}

/// Validate merged text before staging by scanning for unresolved
/// conflict marker lines.
pub fn validate_conflict_resolution_text(text: &str) -> ConflictTextValidation {
    let marker_lines = text
        .lines()
        .filter(|line| {
            line.starts_with("<<<<<<<")
                || line.starts_with(">>>>>>>")
                || line.starts_with("=======")
                || line.starts_with("|||||||")
        })
        .count();

    ConflictTextValidation {
        has_conflict_markers: marker_lines > 0,
        marker_lines,
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConflictFileStages {
    pub path: PathBuf,
    pub base_bytes: Option<Arc<[u8]>>,
    pub ours_bytes: Option<Arc<[u8]>>,
    pub theirs_bytes: Option<Arc<[u8]>>,
    pub base: Option<Arc<str>>,
    pub ours: Option<Arc<str>>,
    pub theirs: Option<Arc<str>>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResetMode {
    Soft,
    Mixed,
    Hard,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RemoteUrlKind {
    Fetch,
    Push,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InteractiveRebaseAction {
    Pick,
    Reword,
    Squash,
    Fixup,
    Drop,
}

impl InteractiveRebaseAction {
    pub fn to_todo_str(self) -> &'static str {
        match self {
            Self::Pick => "pick",
            Self::Reword => "reword",
            Self::Squash => "squash",
            Self::Fixup => "fixup",
            Self::Drop => "drop",
        }
    }

    /// Inverse of [`Self::to_todo_str`], also accepting git's single-letter
    /// abbreviations. `None` for todo commands that are not entry actions
    /// (`exec`, `merge`, `label`, …).
    pub fn from_todo_word(word: &str) -> Option<Self> {
        Some(match word {
            "pick" | "p" => Self::Pick,
            "reword" | "r" => Self::Reword,
            "squash" | "s" => Self::Squash,
            "fixup" | "f" => Self::Fixup,
            "drop" | "d" => Self::Drop,
            _ => return None,
        })
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SequencerState {
    #[default]
    None,
    RebaseOrApply,
    CherryPick,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InteractiveRebaseEntry {
    pub action: InteractiveRebaseAction,
    pub commit_id: String,
    /// Single-line original commit subject (git's `%s`), used for list display
    /// and autosquash grouping. Never reflects `new_message` — display code
    /// derives an edited subject via `squash::split_subject_body`.
    pub summary: String,
    /// Full original commit message (subject + body). Seeds the reword dialog
    /// (`squash::reword_seed_message`) and contributes to combined squash
    /// messages; never edited in place.
    pub message: String,
    /// Full replacement message (subject + body), set only when action is
    /// Reword. Its subject may differ from `summary`.
    pub new_message: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SubmoduleTrustTarget {
    pub submodule_path: PathBuf,
    pub display_source: String,
    pub local_source_path: PathBuf,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SubmoduleTrustDecision {
    Proceed,
    Prompt { sources: Vec<SubmoduleTrustTarget> },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BlameLine {
    pub commit_id: Arc<str>,
    pub author: Arc<str>,
    pub author_time_unix: Option<i64>,
    pub summary: Arc<str>,
    pub body: Option<Arc<str>>,
    pub line: String,
    /// Whether the blamed file existed in the first parent of `commit_id`.
    /// When `false`, "view file at parent commit" is a dead end (this commit
    /// introduced the file), so the UI hides that affordance.
    pub prior_exists: bool,
    /// The file's path at `commit_id`, when it differs from the blamed path
    /// because the file was renamed at/after that commit. `None` means the path
    /// is the same as the blamed path. Used so "view file at this commit" and
    /// "prior revision" navigate using the historical name rather than the
    /// current one (which may not exist in that older tree).
    pub source_path: Option<PathBuf>,
    /// For uncommitted ("Not Committed Yet") lines, the commit the working-tree
    /// change is based on (git blame porcelain `previous`), i.e. the revision to
    /// open for "view file at parent commit". `None` for committed lines (which
    /// resolve their parent from `commit_id`) and for uncommitted lines with no
    /// base (newly added files).
    pub prior_commit: Option<Arc<str>>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CommitOperationOutcome {
    pub local_branch: Option<String>,
    pub pre_head: Option<CommitId>,
    pub post_head: Option<CommitId>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SafePushAfterCommitContext {
    pub amend: bool,
    pub local_branch: Option<String>,
    pub pre_head: Option<CommitId>,
    pub post_head: Option<CommitId>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SafePushAfterCommitTarget {
    pub remote: String,
    pub branch: String,
    pub local_branch: String,
    pub local_head: CommitId,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForcePushLease {
    pub remote: String,
    pub branch: String,
    pub expected: CommitId,
    pub local_branch: String,
    pub local_head: CommitId,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SafePushAfterCommitDecision {
    Push {
        target: SafePushAfterCommitTarget,
    },
    PushSetUpstream {
        target: SafePushAfterCommitTarget,
    },
    Blocked {
        summary: String,
        lease: Option<ForcePushLease>,
    },
}

pub trait GitRepository: Send + Sync {
    fn spec(&self) -> &RepoSpec;

    fn log_history_mode_page(
        &self,
        mode: HistoryMode,
        limit: usize,
        cursor: Option<&LogCursor>,
    ) -> Result<LogPage> {
        match mode {
            HistoryMode::AllBranches => self.log_all_branches_page(limit, cursor),
            HistoryMode::FullReachable
            | HistoryMode::FirstParent
            | HistoryMode::NoMerges
            | HistoryMode::MergesOnly => self.log_head_page(limit, cursor),
        }
    }
    fn log_history_mode_page_cancellable(
        &self,
        mode: HistoryMode,
        limit: usize,
        cursor: Option<&LogCursor>,
        cancellation: &CancellationToken,
    ) -> Result<LogPage> {
        cancellation.check_cancelled()?;
        let page = self.log_history_mode_page(mode, limit, cursor)?;
        cancellation.check_cancelled()?;
        Ok(page)
    }

    /// Like [`Self::log_history_mode_page`], but restricted to commits whose
    /// author matches `author` (case-insensitive substring match against the
    /// author name shown in the UI), cancellable, and reporting the page as it
    /// is built.
    ///
    /// An author filter has to walk history until it has found `limit` matching
    /// commits, which for a rare author means walking all of it — over ten
    /// seconds on a repository with a million commits. `on_chunk` lets the
    /// caller show what has been found so far instead of nothing at all, and
    /// `cancellation` lets a filter the user has moved on from be dropped
    /// rather than waited out.
    ///
    /// Each chunk carries the whole page built up to that point, so chunks are
    /// prefixes of each other and of the returned page, and applying one is
    /// idempotent. The default implementation ignores the filter and reports
    /// nothing; backends that support filtering override this method.
    fn log_history_mode_page_streaming(
        &self,
        mode: HistoryMode,
        author: Option<&str>,
        limit: usize,
        cursor: Option<&LogCursor>,
        cancellation: &CancellationToken,
        on_chunk: &mut dyn FnMut(LogChunk),
    ) -> Result<LogPage> {
        let _ = (author, on_chunk);
        cancellation.check_cancelled()?;
        let page = self.log_history_mode_page(mode, limit, cursor)?;
        cancellation.check_cancelled()?;
        Ok(page)
    }

    /// [`Self::log_history_mode_page_streaming`] for callers with nothing to
    /// cancel and no use for the intermediate pages.
    fn log_history_mode_page_filtered(
        &self,
        mode: HistoryMode,
        author: Option<&str>,
        limit: usize,
        cursor: Option<&LogCursor>,
    ) -> Result<LogPage> {
        self.log_history_mode_page_streaming(
            mode,
            author,
            limit,
            cursor,
            &CancellationToken::new(),
            &mut |_| {},
        )
    }

    fn log_head_page(&self, limit: usize, cursor: Option<&LogCursor>) -> Result<LogPage>;
    fn log_head_page_cancellable(
        &self,
        limit: usize,
        cursor: Option<&LogCursor>,
        cancellation: &CancellationToken,
    ) -> Result<LogPage> {
        cancellation.check_cancelled()?;
        let page = self.log_head_page(limit, cursor)?;
        cancellation.check_cancelled()?;
        Ok(page)
    }
    fn log_all_branches_page(&self, _limit: usize, _cursor: Option<&LogCursor>) -> Result<LogPage> {
        Err(Error::new(ErrorKind::Unsupported(
            "all-branches history is not implemented for this backend",
        )))
    }
    fn log_all_branches_page_cancellable(
        &self,
        limit: usize,
        cursor: Option<&LogCursor>,
        cancellation: &CancellationToken,
    ) -> Result<LogPage> {
        cancellation.check_cancelled()?;
        let page = self.log_all_branches_page(limit, cursor)?;
        cancellation.check_cancelled()?;
        Ok(page)
    }
    fn log_file_page(
        &self,
        _path: &Path,
        _limit: usize,
        _cursor: Option<&LogCursor>,
    ) -> Result<LogPage> {
        Err(Error::new(ErrorKind::Unsupported(
            "file history is not implemented for this backend",
        )))
    }
    fn commit_details(&self, id: &CommitId) -> Result<CommitDetails>;
    /// Files that differ between two points (`from` → `to`), for the
    /// compare-selected-commits feature. `from` is the base/older side, so the
    /// result reads as "what `to` adds/removes relative to `from`". `to = None`
    /// compares `from` against the live working tree. Branch and tag comparisons
    /// resolve their tips to commit ids before calling this.
    fn diff_range_files(
        &self,
        _from: &CommitId,
        _to: Option<&CommitId>,
    ) -> Result<Vec<CommitFileChange>> {
        Err(Error::new(ErrorKind::Unsupported(
            "range file listing is not implemented for this backend",
        )))
    }
    /// Full `%B` messages of the given commits, in input order. Message-only
    /// on purpose: callers like the cherry-pick editor need nothing else, and
    /// implementations should skip the per-commit tree diff `commit_details`
    /// pays for.
    fn commit_messages(&self, ids: &[CommitId]) -> Result<Vec<String>> {
        ids.iter()
            .map(|id| self.commit_details(id).map(|details| details.message))
            .collect()
    }
    /// Stable topological ordering of an arbitrary set of commits. Selected
    /// ancestors precede selected descendants even when unselected commits
    /// lie between them; unrelated commits retain their input order.
    fn topologically_order_commits(&self, _ids: &[CommitId]) -> Result<Vec<CommitId>> {
        Err(Error::new(ErrorKind::Unsupported(
            "topological commit ordering is not implemented for this backend",
        )))
    }
    fn recent_commit_messages(&self, _limit: usize) -> Result<Vec<RecentCommitMessage>> {
        Err(Error::new(ErrorKind::Unsupported(
            "recent commit messages are not implemented for this backend",
        )))
    }
    fn reflog_head(&self, limit: usize) -> Result<Vec<ReflogEntry>>;
    fn current_branch(&self) -> Result<String>;
    fn current_branch_cancellable(&self, cancellation: &CancellationToken) -> Result<String> {
        cancellation.check_cancelled()?;
        let branch = self.current_branch()?;
        cancellation.check_cancelled()?;
        Ok(branch)
    }
    fn head_commit_id(&self) -> Result<Option<CommitId>> {
        Err(Error::new(ErrorKind::Unsupported(
            "reading HEAD commit id is not implemented for this backend",
        )))
    }
    fn list_branches(&self) -> Result<Vec<Branch>>;
    fn list_branches_cancellable(&self, cancellation: &CancellationToken) -> Result<Vec<Branch>> {
        cancellation.check_cancelled()?;
        let branches = self.list_branches()?;
        cancellation.check_cancelled()?;
        Ok(branches)
    }
    fn list_tags(&self) -> Result<Vec<Tag>> {
        Err(Error::new(ErrorKind::Unsupported(
            "tag listing is not implemented for this backend",
        )))
    }
    fn list_tags_cancellable(&self, cancellation: &CancellationToken) -> Result<Vec<Tag>> {
        cancellation.check_cancelled()?;
        let tags = self.list_tags()?;
        cancellation.check_cancelled()?;
        Ok(tags)
    }
    fn list_remote_tags(&self) -> Result<Vec<RemoteTag>> {
        Err(Error::new(ErrorKind::Unsupported(
            "remote tag listing is not implemented for this backend",
        )))
    }
    fn list_remote_tags_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<RemoteTag>> {
        cancellation.check_cancelled()?;
        let tags = self.list_remote_tags()?;
        cancellation.check_cancelled()?;
        Ok(tags)
    }
    fn list_remotes(&self) -> Result<Vec<Remote>>;
    fn list_remotes_cancellable(&self, cancellation: &CancellationToken) -> Result<Vec<Remote>> {
        cancellation.check_cancelled()?;
        let remotes = self.list_remotes()?;
        cancellation.check_cancelled()?;
        Ok(remotes)
    }
    fn list_remote_branches(&self) -> Result<Vec<RemoteBranch>>;
    fn list_remote_branches_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<RemoteBranch>> {
        cancellation.check_cancelled()?;
        let branches = self.list_remote_branches()?;
        cancellation.check_cancelled()?;
        Ok(branches)
    }
    fn worktree_status(&self) -> Result<Vec<FileStatus>> {
        self.status().map(|status| status.unstaged)
    }
    fn worktree_status_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<FileStatus>> {
        cancellation.check_cancelled()?;
        let status = self.worktree_status()?;
        cancellation.check_cancelled()?;
        Ok(status)
    }
    fn staged_status(&self) -> Result<Vec<FileStatus>> {
        self.status().map(|status| status.staged)
    }
    fn staged_status_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<FileStatus>> {
        cancellation.check_cancelled()?;
        let status = self.staged_status()?;
        cancellation.check_cancelled()?;
        Ok(status)
    }
    fn status(&self) -> Result<RepoStatus>;
    fn status_cancellable(&self, cancellation: &CancellationToken) -> Result<RepoStatus> {
        cancellation.check_cancelled()?;
        let status = self.status()?;
        cancellation.check_cancelled()?;
        Ok(status)
    }
    fn upstream_divergence(&self) -> Result<Option<UpstreamDivergence>> {
        Ok(None)
    }
    fn upstream_divergence_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Option<UpstreamDivergence>> {
        cancellation.check_cancelled()?;
        let divergence = self.upstream_divergence()?;
        cancellation.check_cancelled()?;
        Ok(divergence)
    }
    fn diff_unified(&self, target: &DiffTarget) -> Result<String>;
    /// Load and parse unified diff rows for the target.
    ///
    /// Default implementation goes through `diff_unified`; backends may
    /// override for streaming parsing to avoid large monolithic allocations.
    fn diff_parsed(&self, target: &DiffTarget) -> Result<Diff> {
        self.diff_unified(target)
            .map(|text| Diff::from_unified(target.clone(), &text))
    }
    fn diff_parsed_cancellable(
        &self,
        target: &DiffTarget,
        cancellation: &CancellationToken,
    ) -> Result<Diff> {
        cancellation.check_cancelled()?;
        let diff = self.diff_parsed(target)?;
        cancellation.check_cancelled()?;
        Ok(diff)
    }
    fn diff_file_text(&self, _target: &DiffTarget) -> Result<Option<FileDiffText>> {
        Err(Error::new(ErrorKind::Unsupported(
            "file diff view is not implemented for this backend",
        )))
    }
    fn diff_file_text_cancellable(
        &self,
        target: &DiffTarget,
        cancellation: &CancellationToken,
    ) -> Result<Option<FileDiffText>> {
        cancellation.check_cancelled()?;
        let result = self.diff_file_text(target)?;
        cancellation.check_cancelled()?;
        Ok(result)
    }
    fn diff_preview_text_file(
        &self,
        _target: &DiffTarget,
        _side: DiffPreviewTextSide,
    ) -> Result<Option<PathBuf>> {
        Err(Error::new(ErrorKind::Unsupported(
            "preview text file loading is not implemented for this backend",
        )))
    }
    fn diff_preview_text_file_cancellable(
        &self,
        target: &DiffTarget,
        side: DiffPreviewTextSide,
        cancellation: &CancellationToken,
    ) -> Result<Option<PathBuf>> {
        cancellation.check_cancelled()?;
        let result = self.diff_preview_text_file(target, side)?;
        cancellation.check_cancelled()?;
        Ok(result)
    }
    fn diff_file_image(&self, _target: &DiffTarget) -> Result<Option<FileDiffImage>> {
        Err(Error::new(ErrorKind::Unsupported(
            "image diff view is not implemented for this backend",
        )))
    }
    fn diff_file_image_cancellable(
        &self,
        target: &DiffTarget,
        cancellation: &CancellationToken,
    ) -> Result<Option<FileDiffImage>> {
        cancellation.check_cancelled()?;
        let result = self.diff_file_image(target)?;
        cancellation.check_cancelled()?;
        Ok(result)
    }

    fn conflict_file_stages(&self, _path: &Path) -> Result<Option<ConflictFileStages>> {
        Err(Error::new(ErrorKind::Unsupported(
            "conflict stage reading is not implemented for this backend",
        )))
    }

    /// Build a backend-native conflict session for a conflicted path.
    ///
    /// Backends that support conflict stages and conflict-kind detection should
    /// return a populated session; unsupported backends return Unsupported.
    fn conflict_session(&self, _path: &Path) -> Result<Option<ConflictSession>> {
        Err(Error::new(ErrorKind::Unsupported(
            "conflict session loading is not implemented for this backend",
        )))
    }

    fn create_branch(&self, name: &str, target: &CommitId) -> Result<()>;
    fn rename_branch(&self, _old_name: &str, _new_name: &str) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "branch renaming is not implemented for this backend",
        )))
    }
    fn delete_branch(&self, name: &str) -> Result<()>;
    fn delete_branch_force(&self, _name: &str) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "force branch deletion is not implemented for this backend",
        )))
    }
    fn checkout_branch(&self, name: &str) -> Result<()>;
    fn checkout_remote_branch(
        &self,
        _remote: &str,
        _branch: &str,
        _local_branch: &str,
    ) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "remote branch checkout is not implemented for this backend",
        )))
    }
    fn checkout_commit(&self, id: &CommitId) -> Result<()>;
    fn cherry_pick(&self, id: &CommitId) -> Result<()>;
    /// Runs a single cherry-pick. `mainline` is Git's 1-based parent number
    /// for a merge commit and must be `None` for non-merge commits.
    fn cherry_pick_with_output(
        &self,
        _id: &CommitId,
        _commit: bool,
        _mainline: Option<usize>,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git cherry-pick is not implemented for this backend",
        )))
    }
    fn revert(&self, id: &CommitId) -> Result<()>;

    fn stash_create(&self, message: &str, include_untracked: bool) -> Result<()>;
    fn stash_list(&self) -> Result<Vec<StashEntry>>;
    fn stash_list_cancellable(&self, cancellation: &CancellationToken) -> Result<Vec<StashEntry>> {
        cancellation.check_cancelled()?;
        let stashes = self.stash_list()?;
        cancellation.check_cancelled()?;
        Ok(stashes)
    }
    fn stash_apply(&self, index: usize) -> Result<()>;
    fn stash_drop(&self, index: usize) -> Result<()>;

    fn stage(&self, paths: &[&Path]) -> Result<()>;
    fn unstage(&self, paths: &[&Path]) -> Result<()>;
    fn commit(&self, message: &str) -> Result<()>;
    fn commit_with_outcome(&self, message: &str) -> Result<CommitOperationOutcome> {
        self.commit(message)?;
        Ok(CommitOperationOutcome::default())
    }
    fn commit_amend(&self, _message: &str) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "commit amend is not implemented for this backend",
        )))
    }
    fn commit_amend_with_outcome(&self, message: &str) -> Result<CommitOperationOutcome> {
        self.commit_amend(message)?;
        Ok(CommitOperationOutcome::default())
    }

    fn rebase_with_output(&self, _onto: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git rebase is not implemented for this backend",
        )))
    }
    fn rebase_continue_with_output(&self) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git rebase --continue is not implemented for this backend",
        )))
    }
    fn rebase_abort_with_output(&self) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git rebase --abort is not implemented for this backend",
        )))
    }
    fn list_commits_for_interactive_rebase(
        &self,
        _base: &str,
    ) -> Result<Vec<InteractiveRebaseEntry>> {
        Err(Error::new(ErrorKind::Unsupported(
            "listing commits for interactive rebase is not implemented for this backend",
        )))
    }
    fn interactive_rebase_with_output(
        &self,
        _base: &str,
        _entries: &[InteractiveRebaseEntry],
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git rebase -i is not implemented for this backend",
        )))
    }
    fn interactive_cherry_pick_with_output(
        &self,
        _entries: &[InteractiveRebaseEntry],
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "interactive cherry-pick is not implemented for this backend",
        )))
    }
    fn merge_abort_with_output(&self) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git merge --abort is not implemented for this backend",
        )))
    }
    fn rebase_in_progress(&self) -> Result<bool> {
        Ok(false)
    }
    fn rebase_in_progress_cancellable(&self, cancellation: &CancellationToken) -> Result<bool> {
        cancellation.check_cancelled()?;
        let in_progress = self.rebase_in_progress()?;
        cancellation.check_cancelled()?;
        Ok(in_progress)
    }
    fn sequencer_state(&self) -> Result<SequencerState> {
        Ok(if self.rebase_in_progress()? {
            SequencerState::RebaseOrApply
        } else {
            SequencerState::None
        })
    }
    fn sequencer_state_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<SequencerState> {
        cancellation.check_cancelled()?;
        let state = self.sequencer_state()?;
        cancellation.check_cancelled()?;
        Ok(state)
    }

    fn merge_commit_message(&self) -> Result<Option<String>> {
        Ok(None)
    }
    fn merge_commit_message_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Option<String>> {
        cancellation.check_cancelled()?;
        let message = self.merge_commit_message()?;
        cancellation.check_cancelled()?;
        Ok(message)
    }

    fn create_tag_with_output(
        &self,
        _name: &str,
        _target: &str,
        _message: Option<&str>,
        _annotated: bool,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git tag creation is not implemented for this backend",
        )))
    }
    fn delete_tag_with_output(&self, _name: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git tag deletion is not implemented for this backend",
        )))
    }
    fn prune_merged_branches_with_output(&self) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "pruning merged branches is not implemented for this backend",
        )))
    }
    fn prune_local_tags_with_output(&self) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "pruning local tags is not implemented for this backend",
        )))
    }
    fn push_tag_with_output(&self, _remote: &str, _name: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "pushing tags is not implemented for this backend",
        )))
    }
    fn delete_remote_tag_with_output(&self, _remote: &str, _name: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "remote tag deletion is not implemented for this backend",
        )))
    }

    fn add_remote_with_output(&self, _name: &str, _url: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git remote add is not implemented for this backend",
        )))
    }
    fn remove_remote_with_output(&self, _name: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git remote remove is not implemented for this backend",
        )))
    }
    fn set_remote_url_with_output(
        &self,
        _name: &str,
        _url: &str,
        _kind: RemoteUrlKind,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git remote set-url is not implemented for this backend",
        )))
    }

    fn fetch_all(&self) -> Result<()>;
    fn pull(&self, mode: PullMode) -> Result<()>;
    fn push(&self) -> Result<()>;
    fn push_force(&self) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "force push is not implemented for this backend",
        )))
    }
    fn push_set_upstream(&self, _remote: &str, _branch: &str) -> Result<()> {
        Err(Error::new(ErrorKind::Unsupported(
            "pushing with --set-upstream is not implemented for this backend",
        )))
    }

    fn fetch_all_with_output(&self) -> Result<CommandOutput> {
        self.fetch_all()?;
        Ok(CommandOutput::empty_success("git fetch --all"))
    }

    fn fetch_all_with_output_prune(&self, _prune: bool) -> Result<CommandOutput> {
        self.fetch_all_with_output()
    }

    fn pull_with_output(&self, mode: PullMode) -> Result<CommandOutput> {
        self.pull(mode)?;
        Ok(CommandOutput::empty_success("git pull"))
    }

    fn push_with_output(&self) -> Result<CommandOutput> {
        self.push()?;
        Ok(CommandOutput::empty_success("git push"))
    }

    fn push_force_with_output(&self) -> Result<CommandOutput> {
        self.push_force()?;
        Ok(CommandOutput::empty_success("git push --force-with-lease"))
    }

    fn safe_push_after_commit(
        &self,
        _context: &SafePushAfterCommitContext,
    ) -> Result<SafePushAfterCommitDecision> {
        Err(Error::new(ErrorKind::Unsupported(
            "safe push after commit is not implemented for this backend",
        )))
    }

    fn push_after_commit_with_output(
        &self,
        target: &SafePushAfterCommitTarget,
    ) -> Result<CommandOutput> {
        validate_safe_push_after_commit_target(self, target)?;
        self.push_with_output()
    }

    fn push_after_commit_set_upstream_with_output(
        &self,
        target: &SafePushAfterCommitTarget,
    ) -> Result<CommandOutput> {
        validate_safe_push_after_commit_target(self, target)?;
        self.push_set_upstream_with_output(&target.remote, &target.branch)
    }

    fn push_force_with_lease_with_output(&self, lease: &ForcePushLease) -> Result<CommandOutput> {
        let _ = lease;
        Err(Error::new(ErrorKind::Unsupported(
            "oid-specific force push with lease is not implemented for this backend",
        )))
    }

    fn push_set_upstream_with_output(&self, remote: &str, branch: &str) -> Result<CommandOutput> {
        self.push_set_upstream(remote, branch)?;
        Ok(CommandOutput::empty_success(format!(
            "git push --set-upstream {remote} HEAD:refs/heads/{branch}"
        )))
    }

    fn set_upstream_branch_with_output(
        &self,
        _branch: &str,
        _upstream: &str,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "setting a branch upstream is not implemented for this backend",
        )))
    }

    fn unset_upstream_branch_with_output(&self, _branch: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "unsetting a branch upstream is not implemented for this backend",
        )))
    }

    fn delete_remote_branch_with_output(
        &self,
        _remote: &str,
        _branch: &str,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "remote branch deletion is not implemented for this backend",
        )))
    }

    /// Delete several branches on one remote.
    ///
    /// A batch method rather than a caller-side loop because deleting is a push:
    /// one invocation carrying every ref is a single network round trip, where
    /// the loop pays one per branch. The default keeps that loop so backends
    /// that only implement the single-branch call stay correct.
    fn delete_remote_branches_with_output(
        &self,
        remote: &str,
        branches: &[String],
    ) -> Result<CommandOutput> {
        let mut last = CommandOutput::empty_success("git push --delete");
        for branch in branches {
            last = self.delete_remote_branch_with_output(remote, branch)?;
        }
        Ok(last)
    }

    fn commit_amend_with_output(&self, message: &str) -> Result<CommandOutput> {
        self.commit_amend(message)?;
        Ok(CommandOutput::empty_success("git commit --amend"))
    }

    fn pull_branch_with_output(&self, _remote: &str, _branch: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "pulling a specific remote branch is not implemented for this backend",
        )))
    }

    fn merge_ref_with_output(&self, _reference: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "merging a specific ref is not implemented for this backend",
        )))
    }

    fn squash_ref_with_output(&self, _reference: &str) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "squashing a specific ref is not implemented for this backend",
        )))
    }

    /// Builds the default combined message for squashing the linear commit
    /// range `oldest..=head`: the oldest commit's full message first, younger
    /// messages appended as paragraphs.
    fn squash_message_preview(&self, _oldest: &CommitId, _head: &CommitId) -> Result<String> {
        Err(Error::new(ErrorKind::Unsupported(
            "squashing commits is not implemented for this backend",
        )))
    }

    /// Squashes the linear first-parent range `oldest..=expected_head` (which
    /// must end at the current HEAD) into a single commit carrying `message`,
    /// preserving the oldest commit's author. Must not touch the worktree or
    /// index, and must fail without changing refs when HEAD no longer equals
    /// `expected_head`.
    fn squash_commits_with_output(
        &self,
        _oldest: &CommitId,
        _expected_head: &CommitId,
        _message: &str,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "squashing commits is not implemented for this backend",
        )))
    }

    fn reset_with_output(&self, _target: &str, _mode: ResetMode) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "git reset is not implemented for this backend",
        )))
    }

    fn blame_file(&self, _path: &Path, _rev: Option<&str>) -> Result<Vec<BlameLine>> {
        Err(Error::new(ErrorKind::Unsupported(
            "git blame is not implemented for this backend",
        )))
    }

    /// Resolve the path of the file currently known as `path` (at some revision)
    /// to the name it has in `commit`'s tree, following renames. Returns `None`
    /// when it cannot be determined (the caller should then fall back to `path`).
    ///
    /// This lets "view file at this commit" navigate across renames: a file's
    /// name in an older/newer commit may differ from the path the caller holds,
    /// and opening the wrong name would fail because it is absent from that tree.
    fn resolve_file_path_at_commit(
        &self,
        _path: &Path,
        _commit: &CommitId,
    ) -> Result<Option<PathBuf>> {
        Ok(None)
    }

    /// Blame the working-tree content shown on the new side of a staged/unstaged
    /// diff. Lines matching committed history are attributed to their commit;
    /// lines not yet committed are returned as "Not Committed Yet" entries.
    fn blame_worktree_file(&self, _path: &Path, _area: DiffArea) -> Result<Vec<BlameLine>> {
        Err(Error::new(ErrorKind::Unsupported(
            "git blame of working-tree content is not implemented for this backend",
        )))
    }

    fn checkout_conflict_side(&self, _path: &Path, _side: ConflictSide) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "conflict resolution is not implemented for this backend",
        )))
    }

    /// Accept a conflict by explicitly deleting the path and staging removal.
    ///
    /// Used by decision/keep-delete resolvers when the chosen outcome is
    /// "accept deletion" rather than selecting a side's content.
    fn accept_conflict_deletion(&self, _path: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "conflict deletion is not implemented for this backend",
        )))
    }

    /// Restore a conflicted file from stage-1 (base) contents and stage it.
    ///
    /// Useful for decision-style conflicts where users want to explicitly
    /// recover the base version as the resolution result.
    fn checkout_conflict_base(&self, _path: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "base conflict checkout is not implemented for this backend",
        )))
    }

    /// Launch an external mergetool for a conflicted file.
    ///
    /// Materializes BASE, LOCAL, REMOTE temp files from the conflict stages,
    /// invokes the configured (or specified) mergetool, reads back the merged
    /// output, writes it to the worktree, and stages the result.
    fn launch_mergetool(&self, _path: &Path) -> Result<MergetoolResult> {
        Err(Error::new(ErrorKind::Unsupported(
            "external mergetool is not implemented for this backend",
        )))
    }

    fn export_patch_with_output(
        &self,
        _commit_id: &CommitId,
        _dest: &Path,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "patch export is not implemented for this backend",
        )))
    }

    fn apply_patch_with_output(&self, _patch: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "patch apply is not implemented for this backend",
        )))
    }

    fn apply_unified_patch_to_index_with_output(
        &self,
        _patch: &str,
        _reverse: bool,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "index patch apply is not implemented for this backend",
        )))
    }

    fn apply_unified_patch_to_worktree_with_output(
        &self,
        _patch: &str,
        _reverse: bool,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree patch apply is not implemented for this backend",
        )))
    }

    fn list_worktrees(&self) -> Result<Vec<Worktree>> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree listing is not implemented for this backend",
        )))
    }
    fn list_worktrees_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<Worktree>> {
        cancellation.check_cancelled()?;
        let worktrees = self.list_worktrees()?;
        cancellation.check_cancelled()?;
        Ok(worktrees)
    }

    /// Tip-commit author/date/summary for every local and remote-tracking ref,
    /// as `(short refname, metadata)` pairs. Purely decorative — callers render
    /// name-only rows when this is unavailable, so backends may leave it
    /// unimplemented.
    fn list_ref_metadata(&self) -> Result<Vec<(String, RefMetadata)>> {
        Err(Error::new(ErrorKind::Unsupported(
            "ref metadata listing is not implemented for this backend",
        )))
    }
    fn list_ref_metadata_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<(String, RefMetadata)>> {
        cancellation.check_cancelled()?;
        let metadata = self.list_ref_metadata()?;
        cancellation.check_cancelled()?;
        Ok(metadata)
    }

    fn add_worktree_with_output(
        &self,
        _path: &Path,
        _reference: Option<&str>,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree add is not implemented for this backend",
        )))
    }

    fn remove_worktree_with_output(&self, _path: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree remove is not implemented for this backend",
        )))
    }

    fn force_remove_worktree_with_output(&self, _path: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree force remove is not implemented for this backend",
        )))
    }

    fn list_submodules(&self) -> Result<Vec<Submodule>> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule listing is not implemented for this backend",
        )))
    }
    fn list_submodules_cancellable(
        &self,
        cancellation: &CancellationToken,
    ) -> Result<Vec<Submodule>> {
        cancellation.check_cancelled()?;
        let submodules = self.list_submodules()?;
        cancellation.check_cancelled()?;
        Ok(submodules)
    }

    /// The working directory as it is on disk, not `HEAD`'s tree.
    fn list_worktree_files(&self) -> Result<Vec<FileEntry>> {
        Err(Error::new(ErrorKind::Unsupported(
            "worktree file listing is not implemented for this backend",
        )))
    }

    fn list_tree_files_at_commit(&self, _commit_id: &CommitId) -> Result<Vec<FileEntry>> {
        Err(Error::new(ErrorKind::Unsupported(
            "tree file listing at commit is not implemented for this backend",
        )))
    }

    fn submodule_diff_summary(
        &self,
        _target: &crate::domain::DiffTarget,
    ) -> Result<crate::domain::SubmoduleDiffSummary> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule diff summary is not implemented for this backend",
        )))
    }
    fn submodule_diff_summary_cancellable(
        &self,
        target: &crate::domain::DiffTarget,
        cancellation: &CancellationToken,
    ) -> Result<crate::domain::SubmoduleDiffSummary> {
        cancellation.check_cancelled()?;
        let summary = self.submodule_diff_summary(target)?;
        cancellation.check_cancelled()?;
        Ok(summary)
    }

    fn check_submodule_add_trust(
        &self,
        _url: &str,
        _path: &Path,
    ) -> Result<SubmoduleTrustDecision> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule trust checks are not implemented for this backend",
        )))
    }

    fn check_submodule_update_trust(&self) -> Result<SubmoduleTrustDecision> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule trust checks are not implemented for this backend",
        )))
    }

    fn check_submodule_load_trust(&self, _path: &Path) -> Result<SubmoduleTrustDecision> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule trust checks are not implemented for this backend",
        )))
    }

    fn add_submodule_with_output(
        &self,
        _url: &str,
        _path: &Path,
        _branch: Option<&str>,
        _name: Option<&str>,
        _force: bool,
        _approved_sources: &[SubmoduleTrustTarget],
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule add is not implemented for this backend",
        )))
    }

    fn update_submodules_with_output(
        &self,
        _approved_sources: &[SubmoduleTrustTarget],
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule update is not implemented for this backend",
        )))
    }

    fn load_submodule_with_output(
        &self,
        _path: &Path,
        _approved_sources: &[SubmoduleTrustTarget],
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule update is not implemented for this backend",
        )))
    }

    fn change_submodule_pointer_with_output(
        &self,
        _path: &Path,
        _reference: &str,
    ) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule pointer changes are not implemented for this backend",
        )))
    }

    fn remove_submodule_with_output(&self, _path: &Path) -> Result<CommandOutput> {
        Err(Error::new(ErrorKind::Unsupported(
            "submodule remove is not implemented for this backend",
        )))
    }

    fn discard_worktree_changes(&self, paths: &[&Path]) -> Result<()>;
}

fn validate_safe_push_after_commit_target<R: GitRepository + ?Sized>(
    repo: &R,
    target: &SafePushAfterCommitTarget,
) -> Result<()> {
    let current_branch = repo.current_branch()?;
    if current_branch != target.local_branch {
        return Err(Error::new(ErrorKind::Backend(format!(
            "stale push-after-commit target: expected branch {}, but current branch is {}",
            target.local_branch, current_branch
        ))));
    }

    let current_head = repo.head_commit_id()?.ok_or_else(|| {
        Error::new(ErrorKind::Backend(
            "stale push-after-commit target: current HEAD does not point to a commit".to_string(),
        ))
    })?;
    if current_head != target.local_head {
        return Err(Error::new(ErrorKind::Backend(format!(
            "stale push-after-commit target: expected HEAD {}, but current HEAD is {}",
            target.local_head, current_head
        ))));
    }

    Ok(())
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PullMode {
    Default,
    Merge,
    FastForwardIfPossible,
    FastForwardOnly,
    Rebase,
}

pub trait GitBackend: Send + Sync {
    fn open(&self, workdir: &Path) -> Result<Arc<dyn GitRepository>>;

    fn open_cancellable(
        &self,
        workdir: &Path,
        cancellation: &CancellationToken,
    ) -> Result<Arc<dyn GitRepository>> {
        cancellation.check_cancelled()?;
        let repo = self.open(workdir)?;
        cancellation.check_cancelled()?;
        Ok(repo)
    }
}

#[cfg(test)]
mod tests {
    use super::{
        BlameLine, CommandOutput, GitRepository, decode_utf8_optional,
        validate_conflict_resolution_text,
    };
    use crate::domain::{
        Branch, CommitDetails, CommitId, DiffTarget, HistoryMode, LogCursor, LogPage, ReflogEntry,
        Remote, RemoteBranch, RepoSpec, RepoStatus, StashEntry,
    };
    use crate::error::{Error, ErrorKind};
    use std::path::{Path, PathBuf};
    use std::sync::{Arc, Mutex};

    fn unsupported<T>() -> super::Result<T> {
        Err(Error::new(ErrorKind::Unsupported(
            "unused in services history-mode delegation test",
        )))
    }

    struct RecordingHistoryModeRepo {
        spec: RepoSpec,
        calls: Mutex<Vec<(&'static str, usize, Option<String>)>>,
    }

    impl RecordingHistoryModeRepo {
        fn new() -> Self {
            Self {
                spec: RepoSpec {
                    workdir: PathBuf::from("/tmp/recording-history-mode-repo"),
                },
                calls: Mutex::new(Vec::new()),
            }
        }

        fn record(&self, method: &'static str, limit: usize, cursor: Option<&LogCursor>) {
            self.calls.lock().expect("recording mutex").push((
                method,
                limit,
                cursor.map(|cursor| cursor.last_seen.as_ref().to_string()),
            ));
        }

        fn calls(&self) -> Vec<(&'static str, usize, Option<String>)> {
            self.calls.lock().expect("recording mutex").clone()
        }
    }

    impl GitRepository for RecordingHistoryModeRepo {
        fn spec(&self) -> &RepoSpec {
            &self.spec
        }

        fn log_head_page(
            &self,
            limit: usize,
            cursor: Option<&LogCursor>,
        ) -> super::Result<LogPage> {
            self.record("head", limit, cursor);
            Ok(LogPage {
                commits: Vec::new(),
                next_cursor: None,
            })
        }

        fn log_all_branches_page(
            &self,
            limit: usize,
            cursor: Option<&LogCursor>,
        ) -> super::Result<LogPage> {
            self.record("all", limit, cursor);
            Ok(LogPage {
                commits: Vec::new(),
                next_cursor: None,
            })
        }

        fn commit_details(&self, _id: &CommitId) -> super::Result<CommitDetails> {
            unsupported()
        }

        fn reflog_head(&self, _limit: usize) -> super::Result<Vec<ReflogEntry>> {
            unsupported()
        }

        fn current_branch(&self) -> super::Result<String> {
            unsupported()
        }

        fn list_branches(&self) -> super::Result<Vec<Branch>> {
            unsupported()
        }

        fn list_remotes(&self) -> super::Result<Vec<Remote>> {
            unsupported()
        }

        fn list_remote_branches(&self) -> super::Result<Vec<RemoteBranch>> {
            unsupported()
        }

        fn status(&self) -> super::Result<RepoStatus> {
            unsupported()
        }

        fn diff_unified(&self, _target: &DiffTarget) -> super::Result<String> {
            unsupported()
        }

        fn create_branch(&self, _name: &str, _target: &CommitId) -> super::Result<()> {
            unsupported()
        }

        fn delete_branch(&self, _name: &str) -> super::Result<()> {
            unsupported()
        }

        fn checkout_branch(&self, _name: &str) -> super::Result<()> {
            unsupported()
        }

        fn checkout_commit(&self, _id: &CommitId) -> super::Result<()> {
            unsupported()
        }

        fn cherry_pick(&self, _id: &CommitId) -> super::Result<()> {
            unsupported()
        }

        fn revert(&self, _id: &CommitId) -> super::Result<()> {
            unsupported()
        }

        fn stash_create(&self, _message: &str, _include_untracked: bool) -> super::Result<()> {
            unsupported()
        }

        fn stash_list(&self) -> super::Result<Vec<StashEntry>> {
            unsupported()
        }

        fn stash_apply(&self, _index: usize) -> super::Result<()> {
            unsupported()
        }

        fn stash_drop(&self, _index: usize) -> super::Result<()> {
            unsupported()
        }

        fn stage(&self, _paths: &[&Path]) -> super::Result<()> {
            unsupported()
        }

        fn unstage(&self, _paths: &[&Path]) -> super::Result<()> {
            unsupported()
        }

        fn commit(&self, _message: &str) -> super::Result<()> {
            unsupported()
        }

        fn fetch_all(&self) -> super::Result<()> {
            unsupported()
        }

        fn pull(&self, _mode: super::PullMode) -> super::Result<()> {
            unsupported()
        }

        fn push(&self) -> super::Result<()> {
            unsupported()
        }

        fn discard_worktree_changes(&self, _paths: &[&Path]) -> super::Result<()> {
            unsupported()
        }
    }

    // ── validate_conflict_resolution_text ────────────────────────────

    #[test]
    fn validate_conflict_resolution_text_reports_no_markers() {
        let validation = validate_conflict_resolution_text("line 1\nline 2\n");
        assert!(!validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 0);
    }

    #[test]
    fn validate_conflict_resolution_text_counts_marker_lines() {
        let text = "<<<<<<< ours\nx\n=======\ny\n>>>>>>> theirs\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 3);
    }

    #[test]
    fn validate_empty_text_reports_no_markers() {
        let validation = validate_conflict_resolution_text("");
        assert!(!validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 0);
    }

    #[test]
    fn validate_diff3_markers_detected() {
        let text = "<<<<<<< ours\na\n||||||| base\nb\n=======\nc\n>>>>>>> theirs\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 4);
    }

    #[test]
    fn validate_markers_with_branch_annotations_detected() {
        let text = "<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> feature/my-branch\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 3);
    }

    #[test]
    fn validate_partial_marker_set_detected() {
        // Only start marker — still detects it
        let text = "some code\n<<<<<<< HEAD\nmore code\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 1);
    }

    #[test]
    fn validate_markers_not_at_start_of_line_ignored() {
        // Markers must be at line start to count
        let text = "  <<<<<<< not a marker\n  ======= not a marker\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(!validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 0);
    }

    #[test]
    fn validate_multiple_conflicts_counts_all_markers() {
        let text = "\
<<<<<<< HEAD\na\n=======\nb\n>>>>>>> branch1\n\
<<<<<<< HEAD\nc\n=======\nd\n>>>>>>> branch2\n";
        let validation = validate_conflict_resolution_text(text);
        assert!(validation.has_conflict_markers);
        assert_eq!(validation.marker_lines, 6);
    }

    // ── decode_utf8_optional ─────────────────────────────────────────

    #[test]
    fn decode_utf8_none_returns_none() {
        assert_eq!(decode_utf8_optional(None), None);
    }

    #[test]
    fn decode_utf8_valid_returns_string() {
        let bytes = b"hello world";
        assert_eq!(
            decode_utf8_optional(Some(bytes.as_slice())),
            Some("hello world".to_string())
        );
    }

    #[test]
    fn decode_utf8_invalid_returns_none() {
        let bytes = &[0xff, 0xfe, 0x00, 0x01];
        assert_eq!(decode_utf8_optional(Some(bytes.as_slice())), None);
    }

    #[test]
    fn decode_utf8_empty_bytes_returns_empty_string() {
        let bytes: &[u8] = b"";
        assert_eq!(decode_utf8_optional(Some(bytes)), Some(String::new()));
    }

    #[test]
    fn decode_utf8_multibyte_chars_preserved() {
        let text = "héllo wörld 日本語";
        assert_eq!(
            decode_utf8_optional(Some(text.as_bytes())),
            Some(text.to_string())
        );
    }

    // ── CommandOutput ────────────────────────────────────────────────

    #[test]
    fn command_output_empty_success_has_zero_exit_code() {
        let out = CommandOutput::empty_success("git status");
        assert_eq!(out.command, "git status");
        assert_eq!(out.stdout, "");
        assert_eq!(out.stderr, "");
        assert_eq!(out.exit_code, Some(0));
    }

    #[test]
    fn command_output_combined_stdout_only() {
        let out = CommandOutput {
            command: "test".into(),
            stdout: "output line\n".into(),
            stderr: String::new(),
            exit_code: Some(0),
        };
        assert_eq!(out.combined(), "output line");
    }

    #[test]
    fn command_output_combined_stderr_only() {
        let out = CommandOutput {
            command: "test".into(),
            stdout: String::new(),
            stderr: "error message\n".into(),
            exit_code: Some(1),
        };
        assert_eq!(out.combined(), "error message");
    }

    #[test]
    fn command_output_combined_both_streams() {
        let out = CommandOutput {
            command: "test".into(),
            stdout: "output\n".into(),
            stderr: "warning\n".into(),
            exit_code: Some(0),
        };
        assert_eq!(out.combined(), "output\nwarning");
    }

    #[test]
    fn command_output_combined_empty_when_both_blank() {
        let out = CommandOutput {
            command: "test".into(),
            stdout: "   \n".into(),
            stderr: "  \n".into(),
            exit_code: Some(0),
        };
        assert_eq!(out.combined(), "");
    }

    #[test]
    fn command_output_combined_trims_trailing_whitespace() {
        let out = CommandOutput {
            command: "test".into(),
            stdout: "line1\nline2\n\n".into(),
            stderr: "err\n\n".into(),
            exit_code: Some(0),
        };
        assert_eq!(out.combined(), "line1\nline2\nerr");
    }

    #[test]
    fn command_output_default_has_no_exit_code() {
        let out = CommandOutput::default();
        assert_eq!(out.command, "");
        assert_eq!(out.exit_code, None);
    }

    #[test]
    fn blame_line_clone_shares_arc_metadata() {
        let line = BlameLine {
            commit_id: "deadbeef".into(),
            author: "Alice".into(),
            author_time_unix: Some(1_700_000_000),
            summary: "Initial import".into(),
            body: Some("detailed body".into()),
            line: "hello".to_string(),
            prior_exists: true,
            source_path: None,
            prior_commit: None,
        };

        let cloned = line.clone();
        assert!(Arc::ptr_eq(&line.commit_id, &cloned.commit_id));
        assert!(Arc::ptr_eq(&line.author, &cloned.author));
        assert!(Arc::ptr_eq(&line.summary, &cloned.summary));
        assert_eq!(line.line, cloned.line);
    }

    #[test]
    fn log_history_mode_page_delegates_current_branch_modes_to_head_log() {
        let repo = RecordingHistoryModeRepo::new();
        let cursor = LogCursor {
            last_seen: CommitId("cursor".into()),
            resume_from: Some(CommitId("resume".into())),
            resume_token: Some(Arc::from("token")),
        };

        for mode in [
            HistoryMode::FullReachable,
            HistoryMode::FirstParent,
            HistoryMode::NoMerges,
            HistoryMode::MergesOnly,
            HistoryMode::AllBranches,
        ] {
            repo.log_history_mode_page(mode, 7, Some(&cursor))
                .expect("history mode delegation should succeed");
        }

        assert_eq!(
            repo.calls(),
            vec![
                ("head", 7, Some("cursor".to_string())),
                ("head", 7, Some("cursor".to_string())),
                ("head", 7, Some("cursor".to_string())),
                ("head", 7, Some("cursor".to_string())),
                ("all", 7, Some("cursor".to_string())),
            ]
        );
    }
}
