#![forbid(unsafe_code)]
#![deny(clippy::disallowed_types)]

use std::{
    error::Error,
    ffi::OsString,
    fmt,
    io::{BufRead as _, IsTerminal as _, Read as _, Write},
    path::{Path, PathBuf},
    process::ExitCode,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};

use ironstorage::{
    command::{
        CliAction, CommandRequest, EXIT_CONFIG, EXIT_FAILURE, EXIT_SUCCESS, EXIT_UNAVAILABLE,
        GeneratedPresentation, GitConfigRequest, GitRemoteRequest, GitRequest, HelpTopic,
        InputPlan, OtpInputSource, OtpRequest, OtpUriPresentation, completion_script, help_text,
        otp_version_text, parse_from, version_text,
    },
    config::Config,
    crypto::KeyStore,
    generate::{GeneratorConfig, PasswordGenerator},
    git::{GitChangeKind, GitIdentity, GitRepository, PullOutcome},
    kdbx::KdbxImporter,
    mutation::{
        MutationError, NoGitTreeCommitter, TreeCommit, TreeCommitError, TreeCommitter, TreeMutator,
    },
    otp::{OtpCodeOutcome, OtpCodeValidity, OtpError, OtpInput, OtpService},
    presentation::{
        ClipboardError, ClipboardTimeout, ClipboardWait, NativeClipboardManager, QrError, QrMatrix,
    },
    read::{PresentationChannel, ShowOutput, ShowResult, VaultReader},
    recipient::{
        NoGitCommitter, PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
    },
    repository::{DirectoryPath, Repository, SecretBytes},
    secret_store::{
        NativeSecretStore, OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError,
        SecretCachePolicy, SecretProtectionPolicy, SecretStore, SecretStoreBackend,
        SecretStoreError,
    },
    write::{
        EntryCommit, EntryCommitError, EntryCommitter, InsertContent, NoGitEntryCommitter,
        OverwriteDecision, VaultWriter,
    },
};

#[allow(dead_code)]
mod editor;

fn main() -> ExitCode {
    match run() {
        Ok(code) => ExitCode::from(code),
        Err(()) => ExitCode::from(ironstorage::command::EXIT_FAILURE),
    }
}

fn run() -> Result<u8, ()> {
    let stdout = std::io::stdout();
    let stderr = std::io::stderr();
    run_with(std::env::args_os(), stdout.lock(), stderr.lock())
}

fn run_with<I, T, O, E>(arguments: I, mut stdout: O, mut stderr: E) -> Result<u8, ()>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
    O: Write,
    E: Write,
{
    run_with_secret_store(arguments, &mut stdout, &mut stderr, || {
        NativeSecretStore::system(
            SecretCachePolicy::Disabled,
            SecretProtectionPolicy::device_unlocked(),
        )
    })
}

fn run_with_secret_store<I, T, O, E>(
    arguments: I,
    mut stdout: O,
    mut stderr: E,
    open_secret_store: impl FnOnce() -> Result<NativeSecretStore, SecretStoreError>,
) -> Result<u8, ()>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
    O: Write,
    E: Write,
{
    let action = match parse_from(arguments) {
        Ok(action) => action,
        Err(error) => {
            stderr
                .write_all(error.to_string().as_bytes())
                .map_err(|_| ())?;
            return Ok(error.exit_code());
        }
    };
    match action {
        CliAction::Display(text) => {
            stdout.write_all(text.as_bytes()).map_err(|_| ())?;
            Ok(EXIT_SUCCESS)
        }
        CliAction::Run(invocation) => match invocation.request() {
            CommandRequest::Help { topic } => {
                stdout
                    .write_all(help_text(*topic).as_bytes())
                    .map_err(|_| ())?;
                Ok(EXIT_SUCCESS)
            }
            CommandRequest::Version => {
                stdout
                    .write_all(version_text().as_bytes())
                    .map_err(|_| ())?;
                Ok(EXIT_SUCCESS)
            }
            CommandRequest::Otp(OtpRequest::Help) => {
                stdout
                    .write_all(help_text(Some(HelpTopic::Otp)).as_bytes())
                    .map_err(|_| ())?;
                Ok(EXIT_SUCCESS)
            }
            CommandRequest::Otp(OtpRequest::Version) => {
                stdout
                    .write_all(otp_version_text().as_bytes())
                    .map_err(|_| ())?;
                Ok(EXIT_SUCCESS)
            }
            CommandRequest::Otp(OtpRequest::Validate { uri }) => match OtpService::validate(uri) {
                Ok(()) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(&mut stderr, error),
            },
            CommandRequest::Completion { shell } => {
                stdout
                    .write_all(&completion_script(*shell))
                    .map_err(|_| ())?;
                Ok(EXIT_SUCCESS)
            }
            request => match Config::load(invocation.config()) {
                Ok(config) if needs_secret_store(request) => {
                    let mut secrets = match open_secret_store().and_then(|store| {
                        store.unlock()?;
                        Ok(store.with_openpgp_passphrase_prompt(NativeOpenPgpPassphrasePrompt))
                    }) {
                        Ok(secrets) => secrets,
                        Err(error) => {
                            writeln!(stderr, "{error}").map_err(|_| ())?;
                            return Ok(EXIT_UNAVAILABLE);
                        }
                    };
                    execute_secure(&config, request, &mut secrets, &mut stdout, &mut stderr)
                }
                Ok(config) => execute_local(&config, request, &mut stdout, &mut stderr),
                Err(error) => {
                    writeln!(stderr, "{error}").map_err(|_| ())?;
                    Ok(EXIT_CONFIG)
                }
            },
        },
    }
}

fn needs_secret_store(request: &CommandRequest) -> bool {
    matches!(
        request,
        CommandRequest::Show(_)
            | CommandRequest::Init(_)
            | CommandRequest::Grep(_)
            | CommandRequest::Insert(_)
            | CommandRequest::Edit(_)
            | CommandRequest::Generate(_)
            | CommandRequest::Remove(_)
            | CommandRequest::Move(_)
            | CommandRequest::Copy(_)
            | CommandRequest::ImportKdbx(_)
            | CommandRequest::Otp(
                OtpRequest::Code(_)
                    | OtpRequest::Insert(_)
                    | OtpRequest::Append(_)
                    | OtpRequest::Uri(_),
            )
            | CommandRequest::Git(
                GitRequest::Diff { .. }
                    | GitRequest::Fetch { .. }
                    | GitRequest::Pull { .. }
                    | GitRequest::Push { .. }
                    | GitRequest::Sync { .. },
            )
    )
}

fn execute_local<O: Write, E: Write>(
    config: &Config,
    request: &CommandRequest,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    let repository = match Repository::open(config.vault()) {
        Ok(repository) => repository,
        Err(error) => return operation_error(stderr, error),
    };
    match request {
        CommandRequest::List(request) => {
            let directory = match DirectoryPath::parse(request.path.as_deref().unwrap_or_default())
            {
                Ok(directory) => directory,
                Err(error) => return operation_error(stderr, error),
            };
            let keys = match KeyStore::load(config.key_material()) {
                Ok(keys) => keys,
                Err(error) => return operation_error(stderr, error),
            };
            match VaultReader::new(&repository, &keys).list(&directory) {
                Ok(tree) => {
                    stdout
                        .write_all(tree.render_plain().as_bytes())
                        .map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Find(request) => {
            let keys = match KeyStore::load(config.key_material()) {
                Ok(keys) => keys,
                Err(error) => return operation_error(stderr, error),
            };
            match VaultReader::new(&repository, &keys).find(&request.terms) {
                Ok(results) => {
                    stdout
                        .write_all(results.render_plain().as_bytes())
                        .map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Git(request) => execute_local_git(&repository, request, stdout, stderr),
        _ => operation_error(stderr, "the command requires authenticated secret access"),
    }
}

fn execute_local_git<O: Write, E: Write>(
    repository: &Repository,
    request: &GitRequest,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    if matches!(request, GitRequest::Init) {
        return match GitRepository::init(repository, git_identity()) {
            Ok(_) => Ok(EXIT_SUCCESS),
            Err(error) => operation_error(stderr, error),
        };
    }
    let mut git = match GitRepository::open(repository, git_identity()) {
        Ok(git) => git,
        Err(error) => return operation_error(stderr, error),
    };
    let result = match request {
        GitRequest::Status => git.status().and_then(|status| {
            if status.is_clean() {
                writeln!(stdout, "clean").map_err(|_| output_git_error())?;
            } else {
                for change in status.staged() {
                    writeln!(
                        stdout,
                        "staged\t{}\t{}",
                        git_change_name(change.kind()),
                        change.path().display()
                    )
                    .map_err(|_| output_git_error())?;
                }
                for change in status.unstaged() {
                    writeln!(
                        stdout,
                        "unstaged\t{}\t{}",
                        git_change_name(change.kind()),
                        change.path().display()
                    )
                    .map_err(|_| output_git_error())?;
                }
            }
            Ok(())
        }),
        GitRequest::Log { maximum } => git.log(maximum.map(usize::from)).and_then(|entries| {
            for entry in entries {
                writeln!(stdout, "commit {}", entry.id()).map_err(|_| output_git_error())?;
                writeln!(
                    stdout,
                    "Author: {} <{}>",
                    entry.author_name(),
                    entry.author_email()
                )
                .map_err(|_| output_git_error())?;
                writeln!(stdout, "Date: {}", entry.timestamp()).map_err(|_| output_git_error())?;
                writeln!(stdout, "\n    {}\n", entry.message().trim_end())
                    .map_err(|_| output_git_error())?;
            }
            Ok(())
        }),
        GitRequest::Diff { paths } => git
            .diff(&paths.iter().map(PathBuf::from).collect::<Vec<_>>())
            .and_then(|entries| {
                for entry in entries {
                    let kind = match (entry.old(), entry.current()) {
                        (None, Some(_)) => "added",
                        (Some(_), None) => "deleted",
                        _ => "modified",
                    };
                    writeln!(stdout, "{kind}\t{}", entry.path().display())
                        .map_err(|_| output_git_error())?;
                }
                Ok(())
            }),
        GitRequest::Add { paths } => {
            git.stage(&paths.iter().map(PathBuf::from).collect::<Vec<_>>())
        }
        GitRequest::Commit { message } => git.commit(message).and_then(|id| {
            writeln!(stdout, "{id}").map_err(|_| output_git_error())?;
            Ok(())
        }),
        GitRequest::Remote(request) => match request {
            GitRemoteRequest::List => {
                for remote in git.remotes() {
                    writeln!(stdout, "{remote}").map_err(|_| ())?;
                }
                return Ok(EXIT_SUCCESS);
            }
            GitRemoteRequest::GetUrl { name } => git.remote_url(name).and_then(|url| {
                writeln!(stdout, "{url}").map_err(|_| output_git_error())?;
                Ok(())
            }),
            GitRemoteRequest::Add { name, url } => git.add_remote(name, url),
            GitRemoteRequest::SetUrl { name, url } => git.set_remote_url(name, url),
            GitRemoteRequest::Remove { name } => git.remove_remote(name),
        },
        GitRequest::Config(request) => match request {
            GitConfigRequest::Get { key } => git.config_get(key).and_then(|value| {
                if let Some(value) = value {
                    writeln!(stdout, "{value}").map_err(|_| output_git_error())?;
                }
                Ok(())
            }),
            GitConfigRequest::Set { key, value } => git.config_set(key, value),
        },
        GitRequest::Init
        | GitRequest::Fetch { .. }
        | GitRequest::Pull { .. }
        | GitRequest::Push { .. }
        | GitRequest::Sync { .. } => unreachable!("routed before local Git execution"),
    };
    match result {
        Ok(()) => Ok(EXIT_SUCCESS),
        Err(error) => operation_error(stderr, error),
    }
}

fn git_change_name(kind: GitChangeKind) -> &'static str {
    match kind {
        GitChangeKind::Added => "added",
        GitChangeKind::Modified => "modified",
        GitChangeKind::Deleted => "deleted",
    }
}

fn output_git_error() -> ironstorage::git::GitError {
    ironstorage::git::GitError::Io {
        operation: "write command output",
        path: PathBuf::from("stdout"),
    }
}

fn git_identity() -> GitIdentity {
    GitIdentity::new("IronStorage", "ironstorage@localhost")
        .expect("the built-in Git identity is valid")
}

fn execute_secure<B: SecretStoreBackend, O: Write, E: Write>(
    config: &Config,
    request: &CommandRequest,
    secrets: &mut SecretStore<B>,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    execute_secure_with_services(
        config,
        request,
        secrets,
        &mut NativePresentation,
        &mut NativeOtpInteraction,
        current_unix_seconds,
        stdout,
        stderr,
    )
}

#[cfg(test)]
fn execute_secure_with<B: SecretStoreBackend, P: CliPresentation, O: Write, E: Write>(
    config: &Config,
    request: &CommandRequest,
    secrets: &mut SecretStore<B>,
    presentation: &mut P,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    execute_secure_with_services(
        config,
        request,
        secrets,
        presentation,
        &mut UnavailableOtpInteraction,
        current_unix_seconds,
        stdout,
        stderr,
    )
}

#[allow(clippy::too_many_arguments)]
fn execute_secure_with_services<
    B: SecretStoreBackend,
    P: CliPresentation,
    I: CliInteraction,
    O: Write,
    E: Write,
>(
    config: &Config,
    request: &CommandRequest,
    secrets: &mut SecretStore<B>,
    presentation: &mut P,
    interaction: &mut I,
    clock: impl Fn() -> Result<u64, OtpInteractionError>,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    let repository = match Repository::open(config.vault()) {
        Ok(repository) => repository,
        Err(error) => return operation_error(stderr, error),
    };
    let keys = match KeyStore::load(config.key_material()) {
        Ok(keys) => keys,
        Err(error) => return operation_error(stderr, error),
    };
    match request {
        CommandRequest::Init(request) => {
            let path = request.path.as_deref().unwrap_or_default();
            let mut committer = match automatic_committer(&repository, path) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match RecipientPolicyManager::new(&repository, &keys).apply_init(
                request,
                None,
                secrets,
                &mut committer,
            ) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Show(request) => {
            match VaultReader::new(&repository, &keys).execute_show(request, secrets) {
                Ok(ShowOutput::Display(ShowResult::Entry(secret))) => {
                    stdout.write_all(secret.expose()).map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Ok(ShowOutput::Display(ShowResult::Directory(tree))) => {
                    stdout
                        .write_all(tree.render_plain().as_bytes())
                        .map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Ok(ShowOutput::Present(secret)) => {
                    let description = format!("{} line {}", secret.entry(), secret.line());
                    let result = match secret.channel() {
                        PresentationChannel::Clipboard => presentation.clipboard(
                            secret.contents(),
                            config.clipboard_timeout(),
                            &description,
                            stdout,
                        ),
                        PresentationChannel::QrCode => {
                            presentation.qr_code(secret.contents(), stdout)
                        }
                    };
                    match result {
                        Ok(()) => Ok(EXIT_SUCCESS),
                        Err(error) => operation_error(stderr, error),
                    }
                }
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Grep(request) => {
            match VaultReader::new(&repository, &keys).grep(request, secrets) {
                Ok(results) => {
                    stdout
                        .write_all(results.render_plain().expose())
                        .map_err(|_| ())?;
                    Ok(if results.is_empty() {
                        EXIT_FAILURE
                    } else {
                        EXIT_SUCCESS
                    })
                }
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Insert(request) => {
            let writer = VaultWriter::new(&repository, &keys);
            let exists = match writer.entry_exists(&request.entry) {
                Ok(exists) => exists,
                Err(error) => return operation_error(stderr, error),
            };
            let overwrite = if exists && !request.force {
                match interaction.confirm(
                    &format!(
                        "An entry already exists for {}. Overwrite it?",
                        request.entry
                    ),
                    stderr,
                ) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            } else {
                OverwriteDecision::Allow
            };
            if overwrite == OverwriteDecision::Decline {
                return operation_error(stderr, "entry overwrite was declined");
            }
            let contents = match interaction.read_insert(
                request.input_plan(interaction.standard_input_is_terminal()),
                &request.entry,
                stderr,
            ) {
                Ok(contents) => contents,
                Err(error) => return operation_error(stderr, error),
            };
            let mut committer = match automatic_committer(&repository, &request.entry) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match writer.insert(request, contents, overwrite, None, &mut committer) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Edit(request) => {
            let writer = VaultWriter::new(&repository, &keys);
            let session = match writer.begin_edit(request, secrets) {
                Ok(session) => session,
                Err(error) => return operation_error(stderr, error),
            };
            let (replacement, editor_name) = match interaction.edit(session.plaintext(), config) {
                Ok(replacement) => replacement,
                Err(error) => return operation_error(stderr, error),
            };
            let mut committer = match automatic_committer(&repository, &request.entry) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match writer.finish_edit(session, replacement, &editor_name, None, &mut committer) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Generate(request) => {
            let overwrite = if !request.force
                && !request.in_place
                && match VaultWriter::new(&repository, &keys).entry_exists(&request.entry) {
                    Ok(exists) => exists,
                    Err(error) => return operation_error(stderr, error),
                } {
                match interaction.confirm(
                    &format!(
                        "An entry already exists for {}. Overwrite it?",
                        request.entry
                    ),
                    stderr,
                ) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            } else {
                OverwriteDecision::Allow
            };
            let mut committer = match automatic_committer(&repository, &request.entry) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            let outcome =
                match PasswordGenerator::new(&repository, &keys, GeneratorConfig::pass_defaults())
                    .generate(request, overwrite, None, secrets, &mut committer)
                {
                    Ok(outcome) => outcome,
                    Err(error) => return operation_error(stderr, error),
                };
            let result = match request.presentation {
                GeneratedPresentation::Terminal => stdout
                    .write_all(outcome.password().expose())
                    .and_then(|()| stdout.write_all(b"\n"))
                    .map_err(|_| PresentationFailure::Output),
                GeneratedPresentation::Clipboard => presentation.clipboard(
                    outcome.password(),
                    config.clipboard_timeout(),
                    &format!("generated password for {}", request.entry),
                    stdout,
                ),
                GeneratedPresentation::QrCode => presentation.qr_code(outcome.password(), stdout),
            };
            match result {
                Ok(()) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Remove(request) => {
            let decision = if request.force {
                OverwriteDecision::Allow
            } else {
                match interaction.confirm(&format!("Remove {}?", request.entry), stderr) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            };
            let mut committer = match automatic_committer(&repository, &request.entry) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match TreeMutator::new(&repository, &keys).remove(request, decision, &mut committer) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Move(request) => execute_transfer(
            &repository,
            &keys,
            TransferRequest::Move(request),
            secrets,
            interaction,
            stderr,
        ),
        CommandRequest::Copy(request) => execute_transfer(
            &repository,
            &keys,
            TransferRequest::Copy(request),
            secrets,
            interaction,
            stderr,
        ),
        CommandRequest::ImportKdbx(request) => {
            let password = match interaction.read_kdbx_password(request.source()) {
                Ok(password) => password,
                Err(error) => return operation_error(stderr, error),
            };
            match KdbxImporter::new(&repository, &keys).import(
                request,
                password,
                secrets,
                git_identity(),
            ) {
                Ok(outcome) => {
                    writeln!(
                        stdout,
                        "KDBX import: {} added, {} updated, {} unchanged, {} skipped",
                        outcome.added(),
                        outcome.updated(),
                        outcome.unchanged(),
                        outcome.skipped()
                    )
                    .map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::Otp(request) => execute_otp(
            config,
            request,
            secrets,
            presentation,
            interaction,
            clock,
            stdout,
            stderr,
        ),
        CommandRequest::Git(request) => {
            if let GitRequest::Diff { paths } = request {
                let git = match GitRepository::open(&repository, git_identity()) {
                    Ok(git) => git,
                    Err(error) => return operation_error(stderr, error),
                };
                return match git.render_diff(
                    &paths.iter().map(PathBuf::from).collect::<Vec<_>>(),
                    &keys,
                    secrets,
                ) {
                    Ok(diff) => {
                        stdout.write_all(diff.expose()).map_err(|_| ())?;
                        Ok(EXIT_SUCCESS)
                    }
                    Err(error) => operation_error(stderr, error),
                };
            }
            let requested_remote = match request {
                GitRequest::Fetch { remote }
                | GitRequest::Pull { remote, .. }
                | GitRequest::Push { remote, .. }
                | GitRequest::Sync { remote } => remote.as_deref(),
                _ => unreachable!("local Git requests do not require secret access"),
            };
            let configured = match select_remote(config, requested_remote) {
                Some(configured) => configured,
                None => {
                    stderr
                        .write_all(b"the requested HTTPS Git remote is not configured\n")
                        .map_err(|_| ())?;
                    return Ok(EXIT_CONFIG);
                }
            };
            let git = match GitRepository::open(&repository, git_identity()) {
                Ok(git) => git,
                Err(error) => return operation_error(stderr, error),
            };
            let result = match request {
                GitRequest::Fetch { .. } => git.fetch(configured, secrets).map(|_| None),
                GitRequest::Pull { branch, .. } => git
                    .pull(configured, branch.as_deref(), secrets)
                    .map(|outcome| Some(pull_outcome_name(outcome).to_owned())),
                GitRequest::Push { branch, .. } => git
                    .push(configured, branch.as_deref(), secrets)
                    .map(|outcome| Some(format!("{} {}", outcome.remote(), outcome.new_id()))),
                GitRequest::Sync { .. } => git.sync(configured, secrets).map(|(pull, push)| {
                    Some(format!(
                        "{}; {} {}",
                        pull_outcome_name(pull),
                        push.remote(),
                        push.new_id()
                    ))
                }),
                _ => unreachable!("local Git requests do not require secret access"),
            };
            match result {
                Ok(Some(message)) => {
                    writeln!(stdout, "{message}").map_err(|_| ())?;
                    Ok(EXIT_SUCCESS)
                }
                Ok(None) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        CommandRequest::List(_)
        | CommandRequest::Find(_)
        | CommandRequest::Completion { .. }
        | CommandRequest::Help { .. }
        | CommandRequest::Version => unreachable!("request is handled before secure execution"),
    }
}

#[derive(Clone, Copy)]
enum TransferRequest<'a> {
    Move(&'a ironstorage::command::MoveRequest),
    Copy(&'a ironstorage::command::CopyRequest),
}

impl TransferRequest<'_> {
    fn source(&self) -> &str {
        match self {
            Self::Move(request) => &request.source,
            Self::Copy(request) => &request.source,
        }
    }

    fn destination(&self) -> &str {
        match self {
            Self::Move(request) => &request.destination,
            Self::Copy(request) => &request.destination,
        }
    }

    fn force(&self) -> bool {
        match self {
            Self::Move(request) => request.force,
            Self::Copy(request) => request.force,
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn execute_transfer<B: SecretStoreBackend, I: CliInteraction, E: Write>(
    repository: &Repository,
    keys: &KeyStore,
    request: TransferRequest<'_>,
    secrets: &mut SecretStore<B>,
    interaction: &mut I,
    stderr: &mut E,
) -> Result<u8, ()> {
    let mut committer = match automatic_committer(repository, request.source()) {
        Ok(committer) => committer,
        Err(error) => return operation_error(stderr, error),
    };
    let mut decision = if request.force() {
        OverwriteDecision::Allow
    } else {
        OverwriteDecision::Decline
    };
    loop {
        let mutator = TreeMutator::new(repository, keys);
        let result = match request {
            TransferRequest::Move(request) => {
                mutator.move_tree(request, decision, None, secrets, &mut committer)
            }
            TransferRequest::Copy(request) => {
                mutator.copy(request, decision, None, secrets, &mut committer)
            }
        };
        match result {
            Ok(_) => return Ok(EXIT_SUCCESS),
            Err(MutationError::Cancelled)
                if decision == OverwriteDecision::Decline && !request.force() =>
            {
                decision = match interaction.confirm(
                    &format!(
                        "An entry already exists at {}. Overwrite it?",
                        request.destination()
                    ),
                    stderr,
                ) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                };
                if decision == OverwriteDecision::Decline {
                    return operation_error(stderr, MutationError::Cancelled);
                }
            }
            Err(error) => return operation_error(stderr, error),
        }
    }
}

fn pull_outcome_name(outcome: PullOutcome) -> &'static str {
    match outcome {
        PullOutcome::UpToDate => "up to date",
        PullOutcome::FastForward => "fast-forward",
        PullOutcome::Merged => "merged",
    }
}

#[allow(clippy::too_many_arguments)]
fn execute_otp<B: SecretStoreBackend, P: CliPresentation, I: OtpInteraction, O: Write, E: Write>(
    config: &Config,
    request: &OtpRequest,
    secrets: &mut SecretStore<B>,
    presentation: &mut P,
    interaction: &mut I,
    clock: impl Fn() -> Result<u64, OtpInteractionError>,
    stdout: &mut O,
    stderr: &mut E,
) -> Result<u8, ()> {
    let repository = match Repository::open(config.vault()) {
        Ok(repository) => repository,
        Err(error) => return operation_error(stderr, error),
    };
    let keys = match KeyStore::load(config.key_material()) {
        Ok(keys) => keys,
        Err(error) => return operation_error(stderr, error),
    };
    let service = OtpService::new(&repository, &keys);
    match request {
        OtpRequest::Code(request) => {
            let timestamp = match clock() {
                Ok(timestamp) => timestamp,
                Err(error) => return operation_error(stderr, error),
            };
            let outcome = match service.code_automatic(&request.entry, timestamp, None, secrets) {
                Ok(outcome) => outcome,
                Err(error) => return operation_error(stderr, error),
            };
            if request.clipboard {
                write_otp_validity(&outcome, timestamp, stderr)?;
                match presentation.clipboard(
                    outcome.code(),
                    config.clipboard_timeout(),
                    &format!("OTP code for {}", request.entry),
                    stdout,
                ) {
                    Ok(()) => Ok(EXIT_SUCCESS),
                    Err(error) => operation_error(stderr, error),
                }
            } else {
                stdout
                    .write_all(outcome.code().expose())
                    .and_then(|()| stdout.write_all(b"\n"))
                    .map_err(|_| ())?;
                write_otp_validity(&outcome, timestamp, stderr)?;
                Ok(EXIT_SUCCESS)
            }
        }
        OtpRequest::Insert(request) => {
            let input = match interaction.read_input(
                request.input_plan(interaction.standard_input_is_terminal()),
                &request.source,
                request.entry.as_deref().unwrap_or("this token"),
                stderr,
            ) {
                Ok(input) => input,
                Err(error) => return operation_error(stderr, error),
            };
            let plan = match service.prepare_insert(request, input) {
                Ok(plan) => plan,
                Err(error) => return operation_error(stderr, error),
            };
            let path_decision = if plan.requires_path_confirmation() {
                match interaction.confirm(&format!("Insert into {}?", plan.path()), stderr) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            } else {
                OverwriteDecision::Allow
            };
            if path_decision == OverwriteDecision::Decline {
                return operation_error(stderr, OtpError::Cancelled);
            }
            let overwrite = if plan.requires_overwrite_confirmation() {
                match interaction.confirm(
                    &format!("An entry already exists for {}. Overwrite it?", plan.path()),
                    stderr,
                ) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            } else {
                OverwriteDecision::Allow
            };
            let mut committer = match automatic_committer(&repository, &plan.path().to_string()) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match service.finish_insert(plan, path_decision, overwrite, None, &mut committer) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        OtpRequest::Append(request) => {
            let session = match service.begin_append(request, secrets) {
                Ok(session) => session,
                Err(error) => return operation_error(stderr, error),
            };
            let replace = if session.requires_replace_confirmation() {
                match interaction.confirm(
                    &format!(
                        "An OTP secret already exists for {}. Overwrite it?",
                        session.path()
                    ),
                    stderr,
                ) {
                    Ok(decision) => decision,
                    Err(error) => return operation_error(stderr, error),
                }
            } else {
                OverwriteDecision::Allow
            };
            if replace == OverwriteDecision::Decline {
                return operation_error(stderr, OtpError::Cancelled);
            }
            let path = session.path().to_string();
            let input = match interaction.read_input(
                request.input_plan(interaction.standard_input_is_terminal()),
                &request.source,
                &request.entry,
                stderr,
            ) {
                Ok(input) => input,
                Err(error) => return operation_error(stderr, error),
            };
            let mut committer = match automatic_committer(&repository, &path) {
                Ok(committer) => committer,
                Err(error) => return operation_error(stderr, error),
            };
            match service.finish_append(session, input, replace, None, &mut committer) {
                Ok(_) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        OtpRequest::Uri(request) => {
            let uri = match service.uri(&request.entry, secrets) {
                Ok(uri) => uri,
                Err(error) => return operation_error(stderr, error),
            };
            let result = match request.presentation {
                OtpUriPresentation::Terminal => stdout
                    .write_all(uri.encoded().expose())
                    .and_then(|()| stdout.write_all(b"\n"))
                    .map_err(|_| PresentationFailure::Output),
                OtpUriPresentation::Clipboard => presentation.clipboard(
                    uri.encoded(),
                    config.clipboard_timeout(),
                    &format!("OTP key URI for {}", request.entry),
                    stdout,
                ),
                OtpUriPresentation::QrCode => presentation.qr_code(uri.encoded(), stdout),
            };
            match result {
                Ok(()) => Ok(EXIT_SUCCESS),
                Err(error) => operation_error(stderr, error),
            }
        }
        OtpRequest::Validate { .. } | OtpRequest::Help | OtpRequest::Version => {
            Ok(EXIT_UNAVAILABLE)
        }
    }
}

fn write_otp_validity(
    outcome: &OtpCodeOutcome,
    unix_seconds: u64,
    feedback: &mut dyn Write,
) -> Result<(), ()> {
    match outcome.validity() {
        OtpCodeValidity::Timed { .. } => {
            let remaining = outcome.remaining_at(unix_seconds).unwrap_or_default();
            let unit = if remaining == 1 { "second" } else { "seconds" };
            writeln!(feedback, "TOTP code is valid for {remaining} {unit}.").map_err(|_| ())
        }
        OtpCodeValidity::CounterBased { counter } => writeln!(
            feedback,
            "HOTP counter {counter} is counter-based and has no time expiry."
        )
        .map_err(|_| ()),
    }
}

trait OtpInteraction {
    fn standard_input_is_terminal(&self) -> bool;

    fn read_input(
        &mut self,
        plan: InputPlan,
        source: &OtpInputSource,
        prompt: &str,
        stderr: &mut dyn Write,
    ) -> Result<OtpInput, OtpInteractionError>;

    fn confirm(
        &mut self,
        prompt: &str,
        stderr: &mut dyn Write,
    ) -> Result<OverwriteDecision, OtpInteractionError>;
}

trait CliInteraction: OtpInteraction {
    fn read_kdbx_password(&mut self, source: &Path) -> Result<SecretBytes, CliInteractionError>;

    fn read_insert(
        &mut self,
        plan: InputPlan,
        entry: &str,
        stderr: &mut dyn Write,
    ) -> Result<InsertContent, CliInteractionError>;

    fn edit(
        &mut self,
        plaintext: &SecretBytes,
        config: &Config,
    ) -> Result<(SecretBytes, String), CliInteractionError>;
}

struct NativeOtpInteraction;

struct NativeOpenPgpPassphrasePrompt;

impl OpenPgpPassphrasePrompt for NativeOpenPgpPassphrasePrompt {
    fn request_passphrase(
        &mut self,
        key: &ironstorage::crypto::KeyInfo,
    ) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
        if !std::io::stdin().is_terminal() {
            return Err(OpenPgpPassphrasePromptError::Unavailable);
        }
        rpassword::prompt_password(format!(
            "Enter OpenPGP passphrase for {}: ",
            key.fingerprint()
        ))
        .map(|value| SecretBytes::new(value.into_bytes()))
        .map_err(|error| match error.kind() {
            std::io::ErrorKind::Interrupted | std::io::ErrorKind::UnexpectedEof => {
                OpenPgpPassphrasePromptError::Cancelled
            }
            _ => OpenPgpPassphrasePromptError::Unavailable,
        })
    }
}

impl OtpInteraction for NativeOtpInteraction {
    fn standard_input_is_terminal(&self) -> bool {
        std::io::stdin().is_terminal()
    }

    fn read_input(
        &mut self,
        plan: InputPlan,
        source: &OtpInputSource,
        prompt: &str,
        stderr: &mut dyn Write,
    ) -> Result<OtpInput, OtpInteractionError> {
        let subject = match source {
            OtpInputSource::Uri => "otpauth:// URI",
            OtpInputSource::Secret { .. } => "secret",
        };
        match plan {
            InputPlan::HiddenConfirmed => {
                let first = rpassword::prompt_password(format!("Enter {subject} for {prompt}: "))
                    .map_err(|_| OtpInteractionError::Input)?
                    .into_bytes();
                let confirmation =
                    rpassword::prompt_password(format!("Retype {subject} for {prompt}: "))
                        .map_err(|_| OtpInteractionError::Input)?
                        .into_bytes();
                OtpInput::hidden(first, confirmation).map_err(Into::into)
            }
            InputPlan::EchoedLine => {
                write!(stderr, "Enter {subject} for {prompt}: ")
                    .and_then(|()| stderr.flush())
                    .map_err(|_| OtpInteractionError::Output)?;
                OtpInput::line(read_standard_input_line()?).map_err(Into::into)
            }
            InputPlan::StandardInputLine => {
                OtpInput::line(read_standard_input_line()?).map_err(Into::into)
            }
            InputPlan::StandardInputToEnd => Err(OtpInteractionError::Input),
        }
    }

    fn confirm(
        &mut self,
        prompt: &str,
        stderr: &mut dyn Write,
    ) -> Result<OverwriteDecision, OtpInteractionError> {
        loop {
            write!(stderr, "{prompt} [y/N] ")
                .and_then(|()| stderr.flush())
                .map_err(|_| OtpInteractionError::Output)?;
            let answer = read_standard_input_line()?;
            match answer.as_slice() {
                b"y" | b"Y" | b"yes" | b"YES" | b"Yes" => {
                    return Ok(OverwriteDecision::Allow);
                }
                b"" | b"n" | b"N" | b"no" | b"NO" | b"No" => {
                    return Ok(OverwriteDecision::Decline);
                }
                _ => {}
            }
        }
    }
}

impl CliInteraction for NativeOtpInteraction {
    fn read_kdbx_password(&mut self, source: &Path) -> Result<SecretBytes, CliInteractionError> {
        rpassword::prompt_password(format!("Enter password for {}: ", source.display()))
            .map(|password| SecretBytes::new(password.into_bytes()))
            .map_err(|_| CliInteractionError::Input)
    }

    fn read_insert(
        &mut self,
        plan: InputPlan,
        entry: &str,
        stderr: &mut dyn Write,
    ) -> Result<InsertContent, CliInteractionError> {
        match plan {
            InputPlan::HiddenConfirmed => {
                let first = rpassword::prompt_password(format!("Enter password for {entry}: "))
                    .map_err(|_| CliInteractionError::Input)?
                    .into_bytes();
                let confirmation =
                    rpassword::prompt_password(format!("Retype password for {entry}: "))
                        .map_err(|_| CliInteractionError::Input)?
                        .into_bytes();
                InsertContent::hidden(first, confirmation).map_err(Into::into)
            }
            InputPlan::EchoedLine => {
                write!(stderr, "Enter password for {entry}: ")
                    .and_then(|()| stderr.flush())
                    .map_err(|_| CliInteractionError::Output)?;
                InsertContent::echoed(read_cli_input_line()?).map_err(Into::into)
            }
            InputPlan::StandardInputLine => {
                InsertContent::echoed(read_cli_input_line()?).map_err(Into::into)
            }
            InputPlan::StandardInputToEnd => {
                let mut contents = Vec::new();
                std::io::stdin()
                    .lock()
                    .read_to_end(&mut contents)
                    .map_err(|_| CliInteractionError::Input)?;
                Ok(InsertContent::multiline(contents))
            }
        }
    }

    fn edit(
        &mut self,
        plaintext: &SecretBytes,
        config: &Config,
    ) -> Result<(SecretBytes, String), CliInteractionError> {
        let resolved = config
            .resolve_editor()
            .map_err(CliInteractionError::EditorResolution)?;
        let name = resolved.command().program().to_owned();
        let replacement =
            editor::edit_replacement(plaintext, &resolved, &mut editor::NativeEditorHost)
                .map_err(CliInteractionError::Editor)?;
        Ok((replacement, name))
    }
}

#[cfg(test)]
struct UnavailableOtpInteraction;

#[cfg(test)]
impl OtpInteraction for UnavailableOtpInteraction {
    fn standard_input_is_terminal(&self) -> bool {
        false
    }

    fn read_input(
        &mut self,
        _plan: InputPlan,
        _source: &OtpInputSource,
        _prompt: &str,
        _stderr: &mut dyn Write,
    ) -> Result<OtpInput, OtpInteractionError> {
        Err(OtpInteractionError::Input)
    }

    fn confirm(
        &mut self,
        _prompt: &str,
        _stderr: &mut dyn Write,
    ) -> Result<OverwriteDecision, OtpInteractionError> {
        Err(OtpInteractionError::Input)
    }
}

#[cfg(test)]
impl CliInteraction for UnavailableOtpInteraction {
    fn read_kdbx_password(&mut self, _source: &Path) -> Result<SecretBytes, CliInteractionError> {
        Err(CliInteractionError::Input)
    }

    fn read_insert(
        &mut self,
        _plan: InputPlan,
        _entry: &str,
        _stderr: &mut dyn Write,
    ) -> Result<InsertContent, CliInteractionError> {
        Err(CliInteractionError::Input)
    }

    fn edit(
        &mut self,
        _plaintext: &SecretBytes,
        _config: &Config,
    ) -> Result<(SecretBytes, String), CliInteractionError> {
        Err(CliInteractionError::Input)
    }
}

#[derive(Debug)]
enum CliInteractionError {
    Write(ironstorage::write::WriteError),
    EditorResolution(ironstorage::config::EditorError),
    Editor(editor::CliEditorError),
    Input,
    Output,
}

impl fmt::Display for CliInteractionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Write(error) => error.fmt(formatter),
            Self::EditorResolution(error) => error.fmt(formatter),
            Self::Editor(error) => error.fmt(formatter),
            Self::Input => formatter.write_str("command input could not be read"),
            Self::Output => formatter.write_str("command prompt could not be written"),
        }
    }
}

impl From<ironstorage::write::WriteError> for CliInteractionError {
    fn from(error: ironstorage::write::WriteError) -> Self {
        Self::Write(error)
    }
}

impl Error for CliInteractionError {}

#[derive(Debug)]
enum OtpInteractionError {
    Otp(OtpError),
    Input,
    Output,
    Clock,
}

impl fmt::Display for OtpInteractionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Otp(error) => error.fmt(formatter),
            Self::Input => formatter.write_str("OTP input could not be read"),
            Self::Output => formatter.write_str("OTP prompt could not be written"),
            Self::Clock => formatter.write_str("the system clock is before the Unix epoch"),
        }
    }
}

impl From<OtpError> for OtpInteractionError {
    fn from(error: OtpError) -> Self {
        Self::Otp(error)
    }
}

impl Error for OtpInteractionError {}

fn read_standard_input_line() -> Result<Vec<u8>, OtpInteractionError> {
    let mut value = Vec::new();
    let read = std::io::stdin()
        .lock()
        .read_until(b'\n', &mut value)
        .map_err(|_| OtpInteractionError::Input)?;
    if read == 0 {
        return Err(OtpInteractionError::Input);
    }
    if value.ends_with(b"\n") {
        value.pop();
    }
    if value.ends_with(b"\r") {
        value.pop();
    }
    Ok(value)
}

fn read_cli_input_line() -> Result<Vec<u8>, CliInteractionError> {
    let mut value = Vec::new();
    let read = std::io::stdin()
        .lock()
        .read_until(b'\n', &mut value)
        .map_err(|_| CliInteractionError::Input)?;
    if read == 0 {
        return Err(CliInteractionError::Input);
    }
    if value.ends_with(b"\n") {
        value.pop();
    }
    if value.ends_with(b"\r") {
        value.pop();
    }
    Ok(value)
}

fn current_unix_seconds() -> Result<u64, OtpInteractionError> {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .map_err(|_| OtpInteractionError::Clock)
}

enum AutomaticCommitter {
    Git(Box<GitRepository>),
    None {
        entry: NoGitEntryCommitter,
        policy: NoGitCommitter,
        tree: NoGitTreeCommitter,
    },
}

impl EntryCommitter for AutomaticCommitter {
    fn commit(&mut self, change: &EntryCommit) -> Result<(), EntryCommitError> {
        match self {
            Self::Git(git) => EntryCommitter::commit(git.as_mut(), change),
            Self::None { entry, .. } => entry.commit(change),
        }
    }
}

impl PolicyCommitter for AutomaticCommitter {
    fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError> {
        match self {
            Self::Git(git) => PolicyCommitter::commit(git.as_mut(), change),
            Self::None { policy, .. } => policy.commit(change),
        }
    }
}

impl TreeCommitter for AutomaticCommitter {
    fn commit(&mut self, change: &TreeCommit) -> Result<(), TreeCommitError> {
        match self {
            Self::Git(git) => TreeCommitter::commit(git.as_mut(), change),
            Self::None { tree, .. } => tree.commit(change),
        }
    }
}

fn automatic_committer(
    repository: &Repository,
    entry: &str,
) -> Result<AutomaticCommitter, ironstorage::git::GitError> {
    match GitRepository::open_innermost(repository, Path::new(entry), git_identity()) {
        Ok(git) => Ok(AutomaticCommitter::Git(Box::new(git))),
        Err(ironstorage::git::GitError::NotRepository) => Ok(AutomaticCommitter::None {
            entry: NoGitEntryCommitter,
            policy: NoGitCommitter,
            tree: NoGitTreeCommitter,
        }),
        Err(error) => Err(error),
    }
}

trait CliPresentation {
    fn clipboard(
        &mut self,
        value: &SecretBytes,
        timeout: ClipboardTimeout,
        description: &str,
        stdout: &mut dyn Write,
    ) -> Result<(), PresentationFailure>;

    fn qr_code(
        &mut self,
        value: &SecretBytes,
        stdout: &mut dyn Write,
    ) -> Result<(), PresentationFailure>;
}

struct NativePresentation;

impl CliPresentation for NativePresentation {
    fn clipboard(
        &mut self,
        value: &SecretBytes,
        timeout: ClipboardTimeout,
        description: &str,
        stdout: &mut dyn Write,
    ) -> Result<(), PresentationFailure> {
        let cancelled = Arc::new(AtomicBool::new(false));
        let signal = Arc::clone(&cancelled);
        ctrlc::set_handler(move || signal.store(true, Ordering::SeqCst))
            .map_err(|_| PresentationFailure::CancellationHandler)?;
        let mut clipboard = NativeClipboardManager::system(timeout)?;
        let mut output_failed = false;
        let result = clipboard.copy_with(value, |duration| {
            if writeln!(
                stdout,
                "Copied {description} to clipboard. Will restore or clear in {} seconds.",
                duration.as_secs()
            )
            .and_then(|()| stdout.flush())
            .is_err()
            {
                output_failed = true;
                return ClipboardWait::Cancelled;
            }
            wait_for_clipboard(duration, &cancelled)
        });
        if output_failed {
            return Err(PresentationFailure::Output);
        }
        result.map(|_| ()).map_err(Into::into)
    }

    fn qr_code(
        &mut self,
        value: &SecretBytes,
        stdout: &mut dyn Write,
    ) -> Result<(), PresentationFailure> {
        let matrix = QrMatrix::encode(value)?;
        let rendered = matrix.render_terminal();
        stdout
            .write_all(rendered.expose())
            .map_err(|_| PresentationFailure::Output)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PresentationFailure {
    Clipboard(ClipboardError),
    Qr(QrError),
    CancellationHandler,
    Output,
}

impl fmt::Display for PresentationFailure {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Clipboard(error) => error.fmt(formatter),
            Self::Qr(error) => error.fmt(formatter),
            Self::CancellationHandler => {
                formatter.write_str("clipboard cancellation handling is unavailable")
            }
            Self::Output => formatter.write_str("presentation output could not be written"),
        }
    }
}

fn wait_for_clipboard(duration: Duration, cancelled: &AtomicBool) -> ClipboardWait {
    let deadline = Instant::now() + duration;
    loop {
        if cancelled.load(Ordering::SeqCst) {
            return ClipboardWait::Cancelled;
        }
        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() {
            return ClipboardWait::Elapsed;
        }
        std::thread::sleep(remaining.min(Duration::from_millis(50)));
    }
}

impl Error for PresentationFailure {}

impl From<ClipboardError> for PresentationFailure {
    fn from(error: ClipboardError) -> Self {
        Self::Clipboard(error)
    }
}

impl From<QrError> for PresentationFailure {
    fn from(error: QrError) -> Self {
        Self::Qr(error)
    }
}

fn select_remote<'a>(
    config: &'a Config,
    requested: Option<&str>,
) -> Option<&'a ironstorage::config::GitRemote> {
    match requested {
        Some(requested) => config
            .git_remotes()
            .iter()
            .find(|remote| remote.name().as_str() == requested),
        None => config.git_remotes().first(),
    }
}

fn operation_error<E: Write>(stderr: &mut E, error: impl std::fmt::Display) -> Result<u8, ()> {
    writeln!(stderr, "{error}").map_err(|_| ())?;
    Ok(EXIT_FAILURE)
}

#[cfg(test)]
mod tests {
    use std::{
        collections::{BTreeMap, VecDeque},
        error::Error,
        ffi::OsString,
        fs,
        io::Write as _,
        sync::{Arc, Mutex},
    };

    use ironstorage::{
        command::{
            CommandRequest, CopyRequest, EXIT_CONFIG, EXIT_FAILURE, EXIT_SUCCESS, EXIT_UNAVAILABLE,
            EXIT_USAGE, EditRequest, FindRequest, GenerateRequest, GeneratedPresentation,
            GitRequest, GrepRequest, InitRequest, InputPlan, InsertInput, InsertRequest,
            ListRequest, MoveRequest, OtpAppendRequest, OtpCodeRequest, OtpInputSource,
            OtpInsertRequest, OtpRequest, OtpUriPresentation, OtpUriRequest, Presentation,
            RemoveRequest, ShowRequest,
        },
        config::Config,
        crypto::{KeyInfo, KeyStore},
        git::{GitCredentialProvider as _, GitIdentity, GitRepository},
        otp::OtpInput,
        presentation::{ClipboardTimeout, QrMatrix},
        repository::{EntryPath, Repository, SecretBytes},
        secret_store::{
            OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError, SecretCachePolicy,
            SecretLocator, SecretProtection, SecretProtectionPolicy, SecretReference, SecretStore,
            SecretStoreBackend, SecretStoreError,
        },
        write::{InsertContent, OverwriteDecision},
    };

    use super::{
        CliPresentation, OtpInteraction, OtpInteractionError, PresentationFailure, execute_local,
        execute_secure, execute_secure_with, execute_secure_with_services, run_with,
        run_with_secret_store, wait_for_clipboard,
    };

    type TestResult = Result<(), Box<dyn Error>>;

    #[derive(Clone, Default)]
    struct MemoryBackend(Arc<Mutex<BTreeMap<SecretLocator, SecretBytes>>>);

    #[derive(Clone, Default)]
    struct MemoryOpenPgpPrompt(Arc<Mutex<MemoryOpenPgpPromptState>>);

    #[derive(Default)]
    struct MemoryOpenPgpPromptState {
        responses: VecDeque<Result<Vec<u8>, OpenPgpPassphrasePromptError>>,
        requests: Vec<String>,
    }

    impl MemoryOpenPgpPrompt {
        fn with_passphrase(passphrase: &[u8]) -> Self {
            let prompt = Self::default();
            prompt
                .0
                .lock()
                .expect("test mutex")
                .responses
                .push_back(Ok(passphrase.to_vec()));
            prompt
        }

        fn requests(&self) -> Vec<String> {
            self.0.lock().expect("test mutex").requests.clone()
        }
    }

    impl OpenPgpPassphrasePrompt for MemoryOpenPgpPrompt {
        fn request_passphrase(
            &mut self,
            key: &KeyInfo,
        ) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
            let mut state = self.0.lock().expect("test mutex");
            state.requests.push(key.fingerprint().as_str().to_owned());
            state
                .responses
                .pop_front()
                .unwrap_or(Err(OpenPgpPassphrasePromptError::Unavailable))
                .map(SecretBytes::new)
        }
    }

    #[derive(Default)]
    struct MemoryPresentation {
        clipboard: Vec<Vec<u8>>,
        qr: Vec<Vec<u8>>,
    }

    #[derive(Default)]
    struct MemoryOtpInteraction {
        terminal: bool,
        inputs: VecDeque<OtpInput>,
        insert_inputs: VecDeque<ironstorage::write::InsertContent>,
        edit_replacements: VecDeque<SecretBytes>,
        kdbx_passwords: VecDeque<SecretBytes>,
        decisions: VecDeque<OverwriteDecision>,
        plans: Vec<InputPlan>,
        prompts: Vec<String>,
    }

    impl OtpInteraction for MemoryOtpInteraction {
        fn standard_input_is_terminal(&self) -> bool {
            self.terminal
        }

        fn read_input(
            &mut self,
            plan: InputPlan,
            _source: &OtpInputSource,
            prompt: &str,
            _stderr: &mut dyn std::io::Write,
        ) -> Result<OtpInput, OtpInteractionError> {
            self.plans.push(plan);
            self.prompts.push(prompt.to_owned());
            self.inputs.pop_front().ok_or(OtpInteractionError::Input)
        }

        fn confirm(
            &mut self,
            prompt: &str,
            _stderr: &mut dyn std::io::Write,
        ) -> Result<OverwriteDecision, OtpInteractionError> {
            self.prompts.push(prompt.to_owned());
            self.decisions.pop_front().ok_or(OtpInteractionError::Input)
        }
    }

    impl super::CliInteraction for MemoryOtpInteraction {
        fn read_kdbx_password(
            &mut self,
            _source: &std::path::Path,
        ) -> Result<SecretBytes, super::CliInteractionError> {
            self.kdbx_passwords
                .pop_front()
                .ok_or(super::CliInteractionError::Input)
        }

        fn read_insert(
            &mut self,
            _plan: InputPlan,
            _entry: &str,
            _stderr: &mut dyn std::io::Write,
        ) -> Result<ironstorage::write::InsertContent, super::CliInteractionError> {
            self.insert_inputs
                .pop_front()
                .ok_or(super::CliInteractionError::Input)
        }

        fn edit(
            &mut self,
            _plaintext: &SecretBytes,
            _config: &Config,
        ) -> Result<(SecretBytes, String), super::CliInteractionError> {
            self.edit_replacements
                .pop_front()
                .map(|replacement| (replacement, "fixture-editor".to_owned()))
                .ok_or(super::CliInteractionError::Input)
        }
    }

    impl CliPresentation for MemoryPresentation {
        fn clipboard(
            &mut self,
            value: &SecretBytes,
            timeout: ClipboardTimeout,
            description: &str,
            stdout: &mut dyn std::io::Write,
        ) -> Result<(), PresentationFailure> {
            self.clipboard.push(value.expose().to_vec());
            writeln!(
                stdout,
                "Copied {description} for {} seconds.",
                timeout.duration().as_secs()
            )
            .map_err(|_| PresentationFailure::Output)
        }

        fn qr_code(
            &mut self,
            value: &SecretBytes,
            stdout: &mut dyn std::io::Write,
        ) -> Result<(), PresentationFailure> {
            self.qr.push(value.expose().to_vec());
            let matrix = QrMatrix::encode(value)?;
            stdout
                .write_all(matrix.render_terminal().expose())
                .map_err(|_| PresentationFailure::Output)
        }
    }

    impl SecretStoreBackend for MemoryBackend {
        fn create(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
            value: &[u8],
        ) -> Result<(), SecretStoreError> {
            let mut values = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
            if values.contains_key(locator) {
                return Err(SecretStoreError::AlreadyExists);
            }
            values.insert(locator.clone(), SecretBytes::new(value.to_vec()));
            Ok(())
        }

        fn retrieve(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
        ) -> Result<SecretBytes, SecretStoreError> {
            self.0
                .lock()
                .map_err(|_| SecretStoreError::Unavailable)?
                .get(locator)
                .map(|value| SecretBytes::new(value.expose().to_vec()))
                .ok_or(SecretStoreError::Missing)
        }

        fn replace(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
            value: &[u8],
        ) -> Result<(), SecretStoreError> {
            let mut values = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
            let existing = values.get_mut(locator).ok_or(SecretStoreError::Missing)?;
            *existing = SecretBytes::new(value.to_vec());
            Ok(())
        }

        fn delete(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
        ) -> Result<(), SecretStoreError> {
            self.0
                .lock()
                .map_err(|_| SecretStoreError::Unavailable)?
                .remove(locator)
                .map(drop)
                .ok_or(SecretStoreError::Missing)
        }
    }

    #[test]
    fn clipboard_wait_is_interruptible_without_skipping_storage_cleanup() {
        let cancelled = std::sync::atomic::AtomicBool::new(true);
        assert_eq!(
            wait_for_clipboard(std::time::Duration::from_secs(60), &cancelled),
            ironstorage::presentation::ClipboardWait::Cancelled
        );
        let elapsed = std::sync::atomic::AtomicBool::new(false);
        assert_eq!(
            wait_for_clipboard(std::time::Duration::from_millis(1), &elapsed),
            ironstorage::presentation::ClipboardWait::Elapsed
        );
    }

    #[test]
    fn help_and_usage_errors_have_stable_streams_and_exit_codes() -> TestResult {
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        let code = run_with(["ironstorage", "--help"], &mut stdout, &mut stderr)
            .expect("writing to memory cannot fail");
        assert_eq!(code, EXIT_SUCCESS);
        assert!(String::from_utf8(stdout)?.contains("Usage"));
        assert!(stderr.is_empty());

        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        let code = run_with(
            ["ironstorage", "insert", "--echo", "--multiline", "entry"],
            &mut stdout,
            &mut stderr,
        )
        .expect("writing to memory cannot fail");
        assert_eq!(code, EXIT_USAGE);
        assert!(stdout.is_empty());
        assert!(String::from_utf8(stderr)?.contains("cannot be used with"));
        Ok(())
    }

    #[test]
    fn configuration_and_unavailable_operations_have_stable_exit_codes() -> TestResult {
        let temporary = tempfile::tempdir()?;
        let missing = temporary.path().join("missing.toml");
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        let code = run_with(
            [
                OsString::from("ironstorage"),
                OsString::from("--config"),
                missing.into_os_string(),
                OsString::from("show"),
                OsString::from("entry"),
            ],
            &mut stdout,
            &mut stderr,
        )
        .expect("writing to memory cannot fail");
        assert_eq!(code, EXIT_CONFIG);
        assert!(stdout.is_empty());
        assert!(String::from_utf8(stderr)?.contains("configuration file not found"));

        let keys = temporary.path().join("keys");
        fs::create_dir(&keys)?;
        let config = temporary.path().join("config.toml");
        fs::write(
            &config,
            "vault = 'vault'\ndefault_key = 'alice'\nkey_material = 'keys'\n",
        )?;
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        let code = run_with_secret_store(
            [
                OsString::from("ironstorage"),
                OsString::from("--config"),
                config.into_os_string(),
                OsString::from("show"),
                OsString::from("entry"),
            ],
            &mut stdout,
            &mut stderr,
            || Err(SecretStoreError::Unavailable),
        )
        .expect("writing to memory cannot fail");
        assert_eq!(code, EXIT_UNAVAILABLE);
        assert!(stdout.is_empty());
        assert!(String::from_utf8(stderr)?.contains("secret store is unavailable"));
        Ok(())
    }

    #[test]
    fn cli_session_unlocks_protected_entries_and_https_credentials_by_reference() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n[[git.remotes]]\nname = 'origin'\nurl = 'https://example.test/store.git'\nserver_id = 'fixture-server'\napplication_id = 'fixture-app'\n",
                fixtures.join("stores/basic"),
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = SecretStore::new(
            MemoryBackend::default(),
            SecretCachePolicy::Disabled,
            SecretProtectionPolicy::device_unlocked(),
        );
        secrets.unlock()?;
        secrets.create(
            &SecretReference::openpgp_passphrase(FINGERPRINT)?,
            SecretBytes::new(b"fixture-alice-passphrase".to_vec()),
        )?;
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            execute_secure(
                &config,
                &CommandRequest::Show(ShowRequest {
                    entry: Some("email/personal".to_owned()),
                    presentation: Presentation::Terminal,
                }),
                &mut secrets,
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(
            stdout,
            fs::read(fixtures.join("expected/basic/email/personal.txt"))?
        );
        assert!(stderr.is_empty());

        let remote = &config.git_remotes()[0];
        secrets.create(
            &SecretReference::https_git_credential(
                remote.server_id().as_str(),
                remote.application_id().as_str(),
                "fixture-account",
            )?,
            SecretBytes::new(b"fixture-token".to_vec()),
        )?;
        let credential = secrets.credential(remote.server_id(), remote.application_id())?;
        assert_eq!(credential.username(), "fixture-account");
        assert_eq!(credential.password(), b"fixture-token");
        Ok(())
    }

    #[test]
    fn cli_terminal_show_preserves_multiline_field_bytes() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let vault = temporary.path().join("vault");
        fs::create_dir(&vault)?;
        fs::write(vault.join(".gpg-id"), format!("{FINGERPRINT}\n"))?;
        let repository = Repository::open(&vault)?;
        let keys = KeyStore::load(fixtures.join("keys"))?;
        let recipients = keys.resolve_recipients(format!("{FINGERPRINT}\n").as_bytes())?;
        let plaintext =
            b"password\ncomments: Recovery codes:\none\ntwo\nurl: https://example.test\n";
        let ciphertext = keys.encrypt(SecretBytes::new(plaintext.to_vec()), &recipients)?;
        repository.write_entry(&EntryPath::parse("documents/multiline")?, &ciphertext)?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
                vault,
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            execute_secure(
                &config,
                &CommandRequest::Show(ShowRequest {
                    entry: Some("documents/multiline".to_owned()),
                    presentation: Presentation::Terminal,
                }),
                &mut secrets,
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(stdout, plaintext);
        assert!(stderr.is_empty());
        Ok(())
    }

    #[test]
    fn cli_prompts_for_a_missing_openpgp_passphrase_then_stores_and_reuses_it() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        const PASSPHRASE: &[u8] = b"fixture-alice-passphrase";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
                fixtures.join("stores/basic"),
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let prompt = MemoryOpenPgpPrompt::with_passphrase(PASSPHRASE);
        let mut secrets = SecretStore::new(
            MemoryBackend::default(),
            SecretCachePolicy::Disabled,
            SecretProtectionPolicy::device_unlocked(),
        )
        .with_openpgp_passphrase_prompt(prompt.clone());
        secrets.unlock()?;
        let request = CommandRequest::Show(ShowRequest {
            entry: Some("email/personal".to_owned()),
            presentation: Presentation::Terminal,
        });

        for _ in 0..2 {
            let mut stdout = Vec::new();
            let mut stderr = Vec::new();
            assert_eq!(
                execute_secure(&config, &request, &mut secrets, &mut stdout, &mut stderr)
                    .expect("memory output cannot fail"),
                EXIT_SUCCESS
            );
            assert_eq!(
                stdout,
                fs::read(fixtures.join("expected/basic/email/personal.txt"))?
            );
            assert!(stderr.is_empty());
        }

        assert_eq!(prompt.requests(), [FINGERPRINT.to_owned()]);
        assert_eq!(
            secrets
                .retrieve(&SecretReference::openpgp_passphrase(FINGERPRINT)?)?
                .expose(),
            PASSPHRASE
        );
        Ok(())
    }

    #[test]
    fn cli_clipboard_and_qr_paths_never_emit_plaintext() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\nclipboard_timeout_seconds = 1\n",
                fixtures.join("stores/basic"),
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut presentation = MemoryPresentation::default();

        for channel in [
            Presentation::Clipboard {
                line: std::num::NonZeroUsize::new(1).expect("non-zero"),
            },
            Presentation::QrCode {
                line: std::num::NonZeroUsize::new(1).expect("non-zero"),
            },
        ] {
            let mut stdout = Vec::new();
            let mut stderr = Vec::new();
            assert_eq!(
                execute_secure_with(
                    &config,
                    &CommandRequest::Show(ShowRequest {
                        entry: Some("email/personal".to_owned()),
                        presentation: channel,
                    }),
                    &mut secrets,
                    &mut presentation,
                    &mut stdout,
                    &mut stderr,
                )
                .expect("memory output cannot fail"),
                EXIT_SUCCESS
            );
            assert!(
                !stdout
                    .windows(b"correct horse fixture".len())
                    .any(|part| { part == b"correct horse fixture" })
            );
            assert!(stderr.is_empty());
        }
        assert_eq!(
            presentation.clipboard,
            vec![b"correct horse fixture".to_vec()]
        );
        assert_eq!(presentation.qr, vec![b"correct horse fixture".to_vec()]);
        Ok(())
    }

    #[test]
    fn cli_generated_passwords_use_clipboard_and_qr_without_plaintext_output() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let vault = temporary.path().join("vault");
        fs::create_dir(&vault)?;
        fs::copy(fixtures.join("stores/basic/.gpg-id"), vault.join(".gpg-id"))?;
        fs::copy(
            fixtures.join("stores/basic/.gpg-id.sig"),
            vault.join(".gpg-id.sig"),
        )?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\nclipboard_timeout_seconds = 1\n",
                vault,
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut presentation = MemoryPresentation::default();

        for (entry, channel) in [
            ("generated/clip", GeneratedPresentation::Clipboard),
            ("generated/qr", GeneratedPresentation::QrCode),
        ] {
            let mut stdout = Vec::new();
            let mut stderr = Vec::new();
            assert_eq!(
                execute_secure_with(
                    &config,
                    &CommandRequest::Generate(GenerateRequest {
                        entry: entry.to_owned(),
                        length: std::num::NonZeroUsize::new(16),
                        no_symbols: true,
                        force: false,
                        in_place: false,
                        presentation: channel,
                    }),
                    &mut secrets,
                    &mut presentation,
                    &mut stdout,
                    &mut stderr,
                )
                .expect("memory output cannot fail"),
                EXIT_SUCCESS
            );
            let presented = match channel {
                GeneratedPresentation::Clipboard => presentation.clipboard.last(),
                GeneratedPresentation::QrCode => presentation.qr.last(),
                GeneratedPresentation::Terminal => None,
            }
            .expect("generated secret was presented");
            assert_eq!(presented.len(), 16);
            assert!(presented.iter().all(u8::is_ascii_alphanumeric));
            assert!(
                !stdout
                    .windows(presented.len())
                    .any(|part| part == presented)
            );
            assert!(stderr.is_empty());
            assert!(vault.join(format!("{entry}.gpg")).is_file());
        }
        Ok(())
    }

    #[test]
    fn otp_validate_runs_without_configuration_and_redacts_invalid_input() -> TestResult {
        let valid = "otpauth://totp/account?secret=JBSWY3DPEHPK3PXP";
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            run_with(
                ["ironstorage", "otp", "validate", valid],
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(stdout.is_empty());
        assert!(stderr.is_empty());

        let invalid = "otpauth://totp/account?secret=PRIVATE-NOT-BASE32";
        assert_eq!(
            run_with(
                ["ironstorage", "otp", "validate", invalid],
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_FAILURE
        );
        assert!(
            !stderr
                .windows(invalid.len())
                .any(|part| part == invalid.as_bytes())
        );
        Ok(())
    }

    #[test]
    fn cli_otp_code_and_uri_use_secret_safe_presentation_channels() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let vault = temporary.path().join("vault");
        fs::create_dir_all(vault.join("otp"))?;
        for path in [".gpg-id", ".gpg-id.sig", "otp/totp.gpg"] {
            fs::copy(fixtures.join("stores/basic").join(path), vault.join(path))?;
        }
        let repository = Repository::open(&vault)?;
        let identity = GitIdentity::ironstorage();
        let git = GitRepository::init(&repository, identity.clone())?;
        let commits_before = git.log(None)?.len();
        drop(git);
        let git_config = vault.join(".git/config");
        let mut config_file = fs::OpenOptions::new().append(true).open(&git_config)?;
        config_file.write_all(&fs::read(fixtures.join("pass-git-config.ini"))?)?;
        drop(config_file);
        let config_before = fs::read(&git_config)?;
        let ciphertext_before = fs::read(vault.join("otp/totp.gpg"))?;
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\nclipboard_timeout_seconds = 1\n",
                vault,
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut presentation = MemoryPresentation::default();
        let mut interaction = MemoryOtpInteraction::default();

        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Show(ShowRequest {
                    entry: Some("otp/totp".to_owned()),
                    presentation: Presentation::Terminal,
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(59),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(
            stdout
                .windows(b"otpauth://totp".len())
                .any(|part| part == b"otpauth://totp")
        );
        stdout.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Code(OtpCodeRequest {
                    entry: "otp/totp".to_owned(),
                    clipboard: true,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(59),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        let code = presentation.clipboard.last().expect("clipboard code");
        assert_eq!(code.len(), 6);
        assert!(code.iter().all(u8::is_ascii_digit));
        assert!(!stdout.windows(code.len()).any(|part| part == code));
        assert_eq!(
            String::from_utf8_lossy(&stderr),
            "TOTP code is valid for 1 second.\n"
        );
        assert_eq!(fs::read(vault.join("otp/totp.gpg"))?, ciphertext_before);
        assert_eq!(fs::read(&git_config)?, config_before);
        let git = GitRepository::open(&repository, identity)?;
        assert_eq!(git.log(None)?.len(), commits_before);

        stdout.clear();
        stderr.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Code(OtpCodeRequest {
                    entry: "otp/totp".to_owned(),
                    clipboard: false,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(59),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(stdout.len(), 7);
        assert!(stdout[..6].iter().all(u8::is_ascii_digit));
        assert_eq!(
            String::from_utf8_lossy(&stderr),
            "TOTP code is valid for 1 second.\n"
        );

        stdout.clear();
        stderr.clear();
        let uri = fs::read(fixtures.join("expected/basic/otp/totp.txt"))?;
        let uri = uri
            .split(|byte| *byte == b'\n')
            .find(|line| line.starts_with(b"otpauth://"))
            .expect("fixture URI");
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Uri(OtpUriRequest {
                    entry: "otp/totp".to_owned(),
                    presentation: OtpUriPresentation::QrCode,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(59),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(presentation.qr.last().map(Vec::as_slice), Some(uri));
        assert!(!stdout.windows(uri.len()).any(|part| part == uri));
        assert!(stderr.is_empty());

        stdout.clear();
        stderr.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Uri(OtpUriRequest {
                    entry: "otp/totp".to_owned(),
                    presentation: OtpUriPresentation::Clipboard,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(59),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(presentation.clipboard.last().map(Vec::as_slice), Some(uri));
        assert!(!stdout.windows(uri.len()).any(|part| part == uri));
        assert!(stderr.is_empty());
        Ok(())
    }

    #[test]
    fn cli_otp_insert_append_and_hotp_increment_mutate_encrypted_entries() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let vault = temporary.path().join("vault");
        fs::create_dir_all(vault.join("email"))?;
        fs::create_dir_all(vault.join("otp"))?;
        for path in [
            ".gpg-id",
            ".gpg-id.sig",
            "email/personal.gpg",
            "otp/hotp.gpg",
        ] {
            fs::copy(fixtures.join("stores/basic").join(path), vault.join(path))?;
        }
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\nclipboard_timeout_seconds = 1\n",
                vault,
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut presentation = MemoryPresentation::default();
        let mut interaction = MemoryOtpInteraction {
            terminal: true,
            ..MemoryOtpInteraction::default()
        };
        interaction.inputs.push_back(OtpInput::hidden(
            b"JBSWY3DPEHPK3PXP".to_vec(),
            b"JBSWY3DPEHPK3PXP".to_vec(),
        )?);
        interaction.decisions.push_back(OverwriteDecision::Allow);
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Insert(OtpInsertRequest {
                    entry: None,
                    force: false,
                    echo: false,
                    source: OtpInputSource::Secret {
                        issuer: Some("Issuer".to_owned()),
                        account: Some("account".to_owned()),
                    },
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(interaction.plans, vec![InputPlan::HiddenConfirmed]);
        assert!(vault.join("Issuer/account.gpg").is_file());

        interaction.terminal = false;
        interaction.inputs.push_back(OtpInput::line(
            b"otpauth://totp/New:alice?secret=JBSWY3DPEHPK3PXP&issuer=New".to_vec(),
        )?);
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Append(OtpAppendRequest {
                    entry: "email/personal".to_owned(),
                    force: false,
                    echo: false,
                    source: OtpInputSource::Uri,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(
            interaction.plans.last(),
            Some(&InputPlan::StandardInputLine)
        );

        stdout.clear();
        stderr.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Otp(OtpRequest::Code(OtpCodeRequest {
                    entry: "otp/hotp".to_owned(),
                    clipboard: false,
                })),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(stdout.len(), 9);
        assert!(stdout[..8].iter().all(u8::is_ascii_digit));
        assert_eq!(
            String::from_utf8_lossy(&stderr),
            "HOTP counter 1 is counter-based and has no time expiry.\n"
        );

        let repository = Repository::open(&vault)?;
        let keys = ironstorage::crypto::KeyStore::load(fixtures.join("keys"))?;
        let derived = keys.decrypt(
            &repository.read_entry(&EntryPath::parse("Issuer/account")?)?,
            &mut secrets,
        )?;
        assert_eq!(
            derived.expose(),
            b"otpauth://totp/Issuer:account?secret=JBSWY3DPEHPK3PXP&issuer=Issuer\n"
        );
        let appended = keys.decrypt(
            &repository.read_entry(&EntryPath::parse("email/personal")?)?,
            &mut secrets,
        )?;
        assert!(
            appended
                .expose()
                .windows(14)
                .any(|part| part == b"otpauth://totp")
        );
        let hotp = keys.decrypt(
            &repository.read_entry(&EntryPath::parse("otp/hotp")?)?,
            &mut secrets,
        )?;
        assert!(hotp.expose().windows(9).any(|part| part == b"counter=1"));
        Ok(())
    }

    #[test]
    fn complete_cli_dispatch_runs_storage_workflows_and_automatic_commits() -> TestResult {
        const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
        let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility");
        let temporary = tempfile::tempdir()?;
        let vault = temporary.path().join("vault");
        fs::create_dir_all(vault.join("email"))?;
        for path in [".gpg-id", ".gpg-id.sig", "email/personal.gpg"] {
            fs::copy(fixtures.join("stores/basic").join(path), vault.join(path))?;
        }
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
                vault,
                FINGERPRINT,
                fixtures.join("keys"),
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();

        assert_eq!(
            execute_local(
                &config,
                &CommandRequest::Git(GitRequest::Init),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        stdout.clear();
        assert_eq!(
            execute_local(
                &config,
                &CommandRequest::List(ListRequest { path: None }),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(stdout.windows(5).any(|part| part == b"email"));
        stdout.clear();
        assert_eq!(
            execute_local(
                &config,
                &CommandRequest::Find(FindRequest {
                    terms: vec!["PERSONAL".to_owned()],
                }),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(stdout.windows(8).any(|part| part == b"personal"));

        let mut secrets = fixture_secrets(FINGERPRINT)?;
        let mut presentation = MemoryPresentation::default();
        let mut interaction = MemoryOtpInteraction::default();
        stdout.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Grep(GrepRequest {
                    pattern: "alice@example\\.test".to_owned(),
                    ignore_case: false,
                    invert_match: false,
                    line_number: false,
                    fixed_strings: false,
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(stdout.windows(14).any(|part| part == b"email/personal"));

        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Init(InitRequest {
                    path: Some("nested".to_owned()),
                    key_identities: vec![FINGERPRINT.to_owned()],
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        interaction
            .insert_inputs
            .push_back(InsertContent::echoed(b"first secret".to_vec())?);
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Insert(InsertRequest {
                    entry: "nested/new".to_owned(),
                    input: InsertInput::EchoedLine,
                    force: false,
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        interaction
            .edit_replacements
            .push_back(SecretBytes::new(b"edited secret\nmetadata".to_vec()));
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Edit(EditRequest {
                    entry: "nested/new".to_owned(),
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        for request in [
            CommandRequest::Copy(CopyRequest {
                source: "nested/new".to_owned(),
                destination: "nested/copied".to_owned(),
                force: false,
            }),
            CommandRequest::Move(MoveRequest {
                source: "nested/copied".to_owned(),
                destination: "nested/moved".to_owned(),
                force: false,
            }),
        ] {
            assert_eq!(
                execute_secure_with_services(
                    &config,
                    &request,
                    &mut secrets,
                    &mut presentation,
                    &mut interaction,
                    || Ok(0),
                    &mut stdout,
                    &mut stderr,
                )
                .expect("memory output cannot fail"),
                EXIT_SUCCESS
            );
        }
        interaction.decisions.push_back(OverwriteDecision::Allow);
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Remove(RemoveRequest {
                    entry: "nested/moved".to_owned(),
                    recursive: false,
                    force: false,
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );

        let repository = Repository::open(&vault)?;
        let keys = ironstorage::crypto::KeyStore::load(fixtures.join("keys"))?;
        let edited = keys.decrypt(
            &repository.read_entry(&EntryPath::parse("nested/new")?)?,
            &mut secrets,
        )?;
        assert_eq!(edited.expose(), b"edited secret\nmetadata");
        assert!(!vault.join("nested/moved.gpg").exists());

        stdout.clear();
        assert_eq!(
            execute_local(
                &config,
                &CommandRequest::Git(GitRequest::Status),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert_eq!(stdout, b"clean\n");
        stdout.clear();
        assert_eq!(
            execute_local(
                &config,
                &CommandRequest::Git(GitRequest::Log { maximum: None }),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        let log = String::from_utf8(stdout.clone())?;
        for message in [
            "Set GPG id",
            "Add given password",
            "Edit password",
            "Copy nested/new",
            "Rename nested/copied",
            "Remove nested/moved",
        ] {
            assert!(log.contains(message), "missing automatic commit {message}");
        }
        let entry = EntryPath::parse("nested/new")?;
        let recipients = ironstorage::recipient::RecipientPolicyManager::new(&repository, &keys)
            .resolve_for_entry(&entry, None)?;
        let replacement = keys.encrypt(
            SecretBytes::new(b"changed secret\nmetadata".to_vec()),
            recipients.recipients(),
        )?;
        repository.write_entry(&entry, &replacement)?;
        stdout.clear();
        assert_eq!(
            execute_secure_with_services(
                &config,
                &CommandRequest::Git(GitRequest::Diff {
                    paths: vec!["nested/new.gpg".to_owned()],
                }),
                &mut secrets,
                &mut presentation,
                &mut interaction,
                || Ok(0),
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(
            stdout
                .windows(b"-edited secret\n".len())
                .any(|part| part == b"-edited secret\n")
        );
        assert!(
            stdout
                .windows(b"+changed secret\n".len())
                .any(|part| part == b"+changed secret\n")
        );
        assert!(!stdout.windows(5).any(|part| part == b"-----"));
        assert!(stderr.is_empty());
        Ok(())
    }

    #[test]
    fn completion_output_and_source_process_boundary_are_audited() -> TestResult {
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        assert_eq!(
            run_with(
                ["ironstorage", "completion", "bash"],
                &mut stdout,
                &mut stderr,
            )
            .expect("memory output cannot fail"),
            EXIT_SUCCESS
        );
        assert!(stdout.windows(11).any(|part| part == b"ironstorage"));
        assert!(stderr.is_empty());

        let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
        let mut rust_sources = Vec::new();
        collect_rust_sources(&workspace.join("apps"), &mut rust_sources)?;
        collect_rust_sources(&workspace.join("crates"), &mut rust_sources)?;
        let unsafe_block = ["unsafe", " {"].concat();
        let unsafe_function = ["unsafe", " fn"].concat();
        let unsafe_implementation = ["unsafe", " impl"].concat();
        let command_constructor = ["Command", "::new("].concat();
        let process_command = ["process", "::Command"].concat();
        for source in rust_sources {
            let contents = fs::read_to_string(&source)?;
            assert!(
                !contents.contains(&unsafe_block),
                "forbidden block in {}",
                source.display()
            );
            assert!(
                !contents.contains(&unsafe_function),
                "forbidden function in {}",
                source.display()
            );
            assert!(
                !contents.contains(&unsafe_implementation),
                "forbidden implementation in {}",
                source.display()
            );
            if contents.contains(&command_constructor) || contents.contains(&process_command) {
                assert!(
                    source.ends_with("apps/cli/src/editor.rs"),
                    "process launch outside editor adapter: {}",
                    source.display()
                );
            }
        }
        Ok(())
    }

    fn collect_rust_sources(
        directory: &std::path::Path,
        output: &mut Vec<std::path::PathBuf>,
    ) -> std::io::Result<()> {
        for entry in fs::read_dir(directory)? {
            let path = entry?.path();
            if path.is_dir() {
                collect_rust_sources(&path, output)?;
            } else if path.extension().and_then(std::ffi::OsStr::to_str) == Some("rs") {
                output.push(path);
            }
        }
        Ok(())
    }

    fn fixture_secrets(fingerprint: &str) -> Result<SecretStore<MemoryBackend>, SecretStoreError> {
        let secrets = SecretStore::new(
            MemoryBackend::default(),
            SecretCachePolicy::Disabled,
            SecretProtectionPolicy::device_unlocked(),
        );
        secrets.unlock()?;
        secrets.create(
            &SecretReference::openpgp_passphrase(fingerprint)?,
            SecretBytes::new(b"fixture-alice-passphrase".to_vec()),
        )?;
        Ok(secrets)
    }
}
