#![forbid(unsafe_code)]

mod support;

use std::{
    error::Error,
    fs,
    io::Cursor,
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};

use ironstorage::{
    command::{InsertInput, InsertRequest},
    config::{Config, ConfigLoader, GitRemote},
    crypto::{DetachedSignatureBytes, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
    git::{
        GitChangeKind, GitConflictChoice, GitConflictResolution, GitCredential,
        GitCredentialProvider, GitError, GitFetchTransport, GitIdentity, GitOperationControl,
        GitProgressPhase, GitRepository, GitSmartHttpTransport, PullOutcome,
    },
    repository::{Repository, SecretBytes},
    write::{InsertContent, OverwriteDecision, VaultWriter},
};
use pgp::{
    composed::{Deserializable as _, DetachedSignature},
    ser::Serialize as _,
};
use sha1::Digest as _;
use support::compatibility::FixtureSet;

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

fn identity() -> GitIdentity {
    GitIdentity::new("IronStorage Test", "test@ironstorage.invalid").expect("valid identity")
}

fn remote_config(temporary: &tempfile::TempDir) -> Result<Config, Box<dyn Error>> {
    fs::create_dir_all(temporary.path().join("config/keys"))?;
    fs::create_dir_all(temporary.path().join("native"))?;
    fs::create_dir_all(temporary.path().join("vault"))?;
    let config_path = temporary.path().join("config/config.toml");
    fs::write(
        &config_path,
        format!(
            "vault = {:?}\ndefault_key = \"0123456789ABCDEF0123456789ABCDEF01234567\"\nkey_material = \"keys\"\n[[git.remotes]]\nname = \"origin\"\nurl = \"https://example.test/store.git\"\nserver_id = \"server\"\napplication_id = \"application\"\n",
            temporary.path().join("vault")
        ),
    )?;
    Ok(
        ConfigLoader::new(temporary.path().to_owned(), temporary.path().join("native"))
            .load(Some(&config_path))?,
    )
}

#[test]
fn local_git_workflow_stages_commits_diffs_logs_and_deletes() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let store = Repository::open(temporary.path())?;
    let git = GitRepository::init(&store, identity())?;

    fs::write(temporary.path().join(".gpg-id"), b"ALICE\n")?;
    git.stage(&[".gpg-id".into()])?;
    let first = git.commit("Set password store recipients.")?;
    assert_eq!(first.len(), 40);
    assert!(git.status()?.is_clean());

    fs::write(temporary.path().join(".gpg-id"), b"BOB\n")?;
    let status = git.status()?;
    assert_eq!(status.unstaged().len(), 1);
    assert_eq!(status.unstaged()[0].kind(), GitChangeKind::Modified);
    let diff = git.diff(&[])?;
    assert_eq!(diff[0].old(), Some(b"ALICE\n".as_slice()));
    assert_eq!(diff[0].current(), Some(b"BOB\n".as_slice()));
    let fixture = FixtureSet::load()?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let rendered = git.render_diff(&[], &keys, &mut SigningSecret(Vec::new()))?;
    assert!(rendered.expose().windows(7).any(|part| part == b"-ALICE\n"));
    assert!(rendered.expose().windows(5).any(|part| part == b"+BOB\n"));

    git.stage(&[".gpg-id".into()])?;
    assert_eq!(git.status()?.staged()[0].kind(), GitChangeKind::Modified);
    git.commit("Change password store recipients.")?;
    assert_eq!(git.log(None)?.len(), 3);
    assert_eq!(
        git.log(Some(1))?[0].message(),
        "Change password store recipients."
    );
    assert_eq!(git.stage_and_commit(&[".gpg-id".into()], "No-op")?, None);

    fs::remove_file(temporary.path().join(".gpg-id"))?;
    git.stage(&[".gpg-id".into()])?;
    let deletion = git.status()?;
    assert_eq!(deletion.staged().len(), 1, "{deletion:?}");
    assert_eq!(deletion.staged()[0].kind(), GitChangeKind::Deleted);
    git.commit("Remove password store recipients.")?;
    assert!(git.status()?.is_clean());
    Ok(())
}

#[test]
fn nested_repository_selection_is_innermost() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let outer_store = Repository::open(temporary.path())?;
    GitRepository::init(&outer_store, identity())?;
    let root = GitRepository::open_innermost(&outer_store, Path::new(""), identity())?;
    assert_eq!(root.root(), fs::canonicalize(temporary.path())?);
    fs::create_dir(temporary.path().join("nested"))?;
    let inner_store = Repository::open(temporary.path().join("nested"))?;
    GitRepository::init(&inner_store, identity())?;
    fs::write(temporary.path().join("nested/secret.gpg"), b"ciphertext")?;

    let selected =
        GitRepository::open_innermost(&outer_store, Path::new("nested/secret.gpg"), identity())?;
    assert_eq!(
        selected.root(),
        fs::canonicalize(temporary.path().join("nested"))?
    );
    Ok(())
}

#[test]
fn remotes_and_config_are_local_https_only() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let store = Repository::open(temporary.path())?;
    let mut git = GitRepository::init(&store, identity())?;
    for forbidden in [
        "ssh://example.test/store.git",
        "git@example.test:store.git",
        "git://example.test/store.git",
        "file:///tmp/store.git",
        "../store.git",
        "ext::helper command",
        "https://user:secret@example.test/store.git",
    ] {
        assert_eq!(
            git.add_remote("origin", forbidden),
            Err(GitError::ForbiddenRemoteUrl)
        );
    }
    git.add_remote("origin", "https://example.test/store.git")?;
    assert_eq!(git.remotes(), ["origin"]);
    assert_eq!(git.remote_url("origin")?, "https://example.test/store.git");
    git.set_remote_url("origin", "https://example.test/other.git")?;
    assert_eq!(git.remote_url("origin")?, "https://example.test/other.git");
    git.config_set("user.name", "Local User")?;
    assert_eq!(git.config_get("user.name")?.as_deref(), Some("Local User"));
    assert!(git.config_set("credential.helper", "evil").is_err());
    assert!(
        git.config_set("diff.gpg.textconv", "external-helper")
            .is_err()
    );
    git.remove_remote("origin")?;
    assert!(git.remotes().is_empty());
    Ok(())
}

struct Credentials;

impl GitCredentialProvider for Credentials {
    fn credential(
        &self,
        server: &ironstorage::config::ServerId,
        application: &ironstorage::config::ApplicationId,
    ) -> Result<GitCredential, GitError> {
        assert_eq!(server.as_str(), "server");
        assert_eq!(application.as_str(), "application");
        GitCredential::new("alice", b"token".to_vec())
    }
}

struct SigningSecret(Vec<u8>);

impl SecretProvider for SigningSecret {
    fn secret_for(&mut self, _key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
        Ok(SecretBytes::new(self.0.clone()))
    }
}

#[derive(Default)]
struct RecordingTransport {
    request: Mutex<Vec<u8>>,
}

struct CloningFetch;

impl GitFetchTransport for CloningFetch {
    fn fetch(
        &self,
        repository: &GitRepository,
        _configured: &GitRemote,
        credential: &GitCredential,
    ) -> Result<bool, GitError> {
        assert_eq!(credential.password(), b"token");
        fs::write(repository.root().join(".gpg-id"), b"ALICE\n")
            .map_err(|error| GitError::InvalidRepository(error.to_string()))?;
        repository.stage(&[".gpg-id".into()])?;
        let tip = repository.commit("Fetched initial store.")?;
        set_remote_tracking(repository.root(), &tip)
            .map_err(|error| GitError::InvalidRepository(error.to_string()))?;
        reset_to_unborn(repository.root())
            .map_err(|error| GitError::InvalidRepository(error.to_string()))?;
        Ok(true)
    }
}

struct OccupyingFetch(PathBuf);

impl GitFetchTransport for OccupyingFetch {
    fn fetch(
        &self,
        repository: &GitRepository,
        configured: &GitRemote,
        credential: &GitCredential,
    ) -> Result<bool, GitError> {
        let fetched = CloningFetch.fetch(repository, configured, credential)?;
        fs::write(self.0.join("keep"), b"concurrent contents")
            .map_err(|error| GitError::InvalidRepository(error.to_string()))?;
        Ok(fetched)
    }
}

struct NoopFetch;

impl GitFetchTransport for NoopFetch {
    fn fetch(
        &self,
        _repository: &GitRepository,
        _configured: &GitRemote,
        credential: &GitCredential,
    ) -> Result<bool, GitError> {
        assert_eq!(credential.username(), "alice");
        Ok(false)
    }
}

struct FailedFetch(GitError);

impl GitFetchTransport for FailedFetch {
    fn fetch(
        &self,
        _repository: &GitRepository,
        _configured: &GitRemote,
        _credential: &GitCredential,
    ) -> Result<bool, GitError> {
        Err(self.0.clone())
    }
}

struct AuthenticationFailure;

impl GitSmartHttpTransport for AuthenticationFailure {
    fn advertise_receive_pack(
        &self,
        _url: &url::Url,
        _credential: &GitCredential,
    ) -> Result<Vec<u8>, GitError> {
        Err(GitError::AuthenticationFailed)
    }

    fn receive_pack(
        &self,
        _url: &url::Url,
        _credential: &GitCredential,
        _request: Vec<u8>,
    ) -> Result<Vec<u8>, GitError> {
        unreachable!("authentication fails before receive-pack")
    }
}

struct NonFastForwardAdvertisement;

impl GitSmartHttpTransport for NonFastForwardAdvertisement {
    fn advertise_receive_pack(
        &self,
        _url: &url::Url,
        _credential: &GitCredential,
    ) -> Result<Vec<u8>, GitError> {
        let mut output = packet(b"# service=git-receive-pack\n");
        output.extend_from_slice(b"0000");
        output.extend_from_slice(&packet(
            b"1111111111111111111111111111111111111111 refs/heads/main\0report-status\n",
        ));
        output.extend_from_slice(b"0000");
        Ok(output)
    }

    fn receive_pack(
        &self,
        _url: &url::Url,
        _credential: &GitCredential,
        _request: Vec<u8>,
    ) -> Result<Vec<u8>, GitError> {
        unreachable!("non-fast-forward is rejected before receive-pack")
    }
}

impl GitSmartHttpTransport for RecordingTransport {
    fn advertise_receive_pack(
        &self,
        _url: &url::Url,
        credential: &GitCredential,
    ) -> Result<Vec<u8>, GitError> {
        assert_eq!(credential.username(), "alice");
        let mut output = packet(b"# service=git-receive-pack\n");
        output.extend_from_slice(b"0000");
        output.extend_from_slice(&packet(b"0000000000000000000000000000000000000000 capabilities^{}\0report-status delete-refs\n"));
        output.extend_from_slice(b"0000");
        Ok(output)
    }

    fn receive_pack(
        &self,
        _url: &url::Url,
        _credential: &GitCredential,
        request: Vec<u8>,
    ) -> Result<Vec<u8>, GitError> {
        *self.request.lock().expect("request lock") = request;
        let mut output = packet(b"unpack ok\n");
        output.extend_from_slice(&packet(b"ok refs/heads/main\n"));
        output.extend_from_slice(b"0000");
        Ok(output)
    }
}

#[test]
fn injected_smart_http_push_sends_a_complete_pack_and_credentials() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote: &GitRemote = &config.git_remotes()[0];
    let store = Repository::open(config.vault())?;
    let mut git = GitRepository::init(&store, identity())?;
    git.add_remote("origin", remote.url().as_str())?;
    fs::write(config.vault().join("secret.gpg"), b"ciphertext")?;
    git.stage(&["secret.gpg".into()])?;
    let head = git.commit("Add secret to store.")?;

    let transport = RecordingTransport::default();
    let outcome = git.push_with_transport(remote, Some("main"), &Credentials, &transport)?;
    assert_eq!(outcome.new_id(), head);
    let request = transport.request.lock().expect("request lock");
    let pack_offset = request
        .windows(4)
        .position(|window| window == b"PACK")
        .expect("pack payload");
    assert!(
        request[..pack_offset]
            .windows(b"refs/heads/main".len())
            .any(|window| window == b"refs/heads/main")
    );
    let pack = &request[pack_offset..];
    assert_eq!(&pack[..4], b"PACK");
    assert_eq!(u32::from_be_bytes(pack[4..8].try_into()?), 2);
    assert!(u32::from_be_bytes(pack[8..12].try_into()?) >= 3);
    let digest = sha1::Sha1::digest(&pack[..pack.len() - 20]);
    assert_eq!(digest.as_slice(), &pack[pack.len() - 20..]);
    Ok(())
}

#[test]
fn controlled_sync_pulls_then_pushes_with_one_progress_and_cancellation_contract() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote = &config.git_remotes()[0];
    let store = Repository::open(config.vault())?;
    let mut git = GitRepository::init(&store, identity())?;
    git.add_remote("origin", remote.url().as_str())?;
    let head = git.log(Some(1))?[0].id().to_owned();
    set_remote_tracking(config.vault(), &head)?;
    let phases = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&phases);
    let control = GitOperationControl::new(move |phase| {
        recorded.lock().expect("progress").push(phase);
    });
    let push = RecordingTransport::default();
    let (pull, pushed) =
        git.sync_with_transports_controlled(remote, &Credentials, &NoopFetch, &push, &control)?;
    assert_eq!(pull, PullOutcome::UpToDate);
    assert_eq!(pushed.new_id(), head);
    let phases = phases.lock().expect("progress");
    assert!(phases.contains(&GitProgressPhase::Receiving));
    assert!(phases.contains(&GitProgressPhase::Integrating));
    assert!(phases.contains(&GitProgressPhase::Sending));
    assert!(!push.request.lock().expect("push request").is_empty());
    Ok(())
}

#[test]
fn push_propagates_authentication_and_rejects_non_fast_forward_before_upload() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote = &config.git_remotes()[0];
    let store = Repository::open(config.vault())?;
    let mut git = GitRepository::init(&store, identity())?;
    git.add_remote("origin", remote.url().as_str())?;
    assert_eq!(
        git.push_with_transport(remote, Some("main"), &Credentials, &AuthenticationFailure),
        Err(GitError::AuthenticationFailed)
    );
    assert_eq!(
        git.push_with_transport(
            remote,
            Some("main"),
            &Credentials,
            &NonFastForwardAdvertisement,
        ),
        Err(GitError::NonFastForward)
    );
    Ok(())
}

#[test]
fn fetched_branches_fast_forward_and_report_typed_conflicts() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote = &config.git_remotes()[0];
    let store = Repository::open(config.vault())?;
    let mut git = GitRepository::init(&store, identity())?;
    git.add_remote("origin", remote.url().as_str())?;

    fs::write(config.vault().join("secret.gpg"), b"base")?;
    git.stage(&["secret.gpg".into()])?;
    let base = git.commit("Base")?;
    fs::write(config.vault().join("secret.gpg"), b"remote")?;
    git.stage(&["secret.gpg".into()])?;
    let remote_tip = git.commit("Remote change")?;

    reset_head_and_index(&git, config.vault(), &base, b"base")?;
    set_remote_tracking(config.vault(), &remote_tip)?;
    assert_eq!(
        git.pull_with_transport(remote, Some("main"), &Credentials, &NoopFetch)?,
        PullOutcome::FastForward
    );
    assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"remote");

    reset_head_and_index(&git, config.vault(), &base, b"base")?;
    fs::write(config.vault().join("secret.gpg"), b"local")?;
    git.stage(&["secret.gpg".into()])?;
    git.commit("Local change")?;
    let error = git
        .pull_with_transport(remote, Some("main"), &Credentials, &NoopFetch)
        .expect_err("conflicting histories");
    let GitError::MergeConflicts { conflicts } = error else {
        panic!("expected typed merge conflicts");
    };
    assert_eq!(conflicts.len(), 1);
    assert_eq!(conflicts[0].path(), Path::new("secret.gpg"));
    assert_eq!(
        conflicts[0].kind(),
        ironstorage::git::GitConflictKind::Content
    );
    assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"local");
    let snapshot = git.snapshot(Some(remote), 5)?;
    assert_eq!(snapshot.remote().expect("remote status").ahead(), 1);
    assert_eq!(snapshot.remote().expect("remote status").behind(), 1);
    let divergence = git.divergence(remote, 10)?;
    assert_eq!(divergence.remote().ahead(), 1);
    assert_eq!(divergence.remote().behind(), 1);
    assert_eq!(divergence.incoming().len(), 1);
    assert_eq!(divergence.incoming()[0].commit().message(), "Remote change");
    assert_eq!(divergence.incoming()[0].changes().len(), 1);
    assert_eq!(
        divergence.incoming()[0].changes()[0].path(),
        Path::new("secret.gpg")
    );
    assert_eq!(divergence.outgoing().len(), 1);
    assert_eq!(divergence.outgoing()[0].commit().message(), "Local change");
    let totals_only = git.divergence(remote, 0)?;
    assert_eq!(totals_only.remote().ahead(), 1);
    assert_eq!(totals_only.remote().behind(), 1);
    assert!(totals_only.incoming().is_empty());
    assert!(totals_only.outgoing().is_empty());

    assert_eq!(
        git.resolve_fetched(
            remote,
            Some("main"),
            &[GitConflictResolution::new(
                "secret.gpg".into(),
                GitConflictChoice::Remote,
            )],
        )?,
        PullOutcome::Merged
    );
    assert_eq!(fs::read(config.vault().join("secret.gpg"))?, b"remote");
    assert_eq!(git.log(Some(1))?[0].parents().len(), 2);
    Ok(())
}

#[test]
fn cancelled_operations_stop_before_credentials_or_transport() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote = &config.git_remotes()[0];
    let store = Repository::open(config.vault())?;
    let mut git = GitRepository::init(&store, identity())?;
    git.add_remote("origin", remote.url().as_str())?;
    let control = GitOperationControl::default();
    control.cancel();
    assert_eq!(
        git.pull_with_transport_controlled(
            remote,
            Some("main"),
            &Credentials,
            &NoopFetch,
            &control,
        ),
        Err(GitError::Cancelled)
    );
    assert!(git.status()?.is_clean());
    let phases = Arc::new(Mutex::new(Vec::new()));
    let recorded = Arc::clone(&phases);
    let progress = GitOperationControl::new(move |phase| {
        recorded.lock().expect("progress lock").push(phase);
    });
    assert!(matches!(
        git.pull_with_transport_controlled(
            remote,
            Some("main"),
            &Credentials,
            &NoopFetch,
            &progress,
        ),
        Err(GitError::RemoteNotFound { .. })
    ));
    assert_eq!(
        *phases.lock().expect("progress lock"),
        [
            GitProgressPhase::Validating,
            GitProgressPhase::Authenticating,
            GitProgressPhase::Receiving,
            GitProgressPhase::Receiving,
            GitProgressPhase::Integrating,
        ]
    );
    for expected in [GitError::NetworkUnavailable, GitError::TlsFailed] {
        assert_eq!(
            git.pull_with_transport(
                remote,
                Some("main"),
                &Credentials,
                &FailedFetch(expected.clone()),
            ),
            Err(expected)
        );
    }
    let credential = GitCredential::new("alice", b"DO-NOT-RENDER".to_vec())?;
    assert_eq!(format!("{credential:?}"), "GitCredential([REDACTED])");
    Ok(())
}

#[test]
fn clone_uses_a_private_directory_and_injected_fetch_transport() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let destination = temporary.path().join("cloned-vault");
    let cloned = GitRepository::clone_into_with_transport(
        &destination,
        identity(),
        &config.git_remotes()[0],
        &Credentials,
        &CloningFetch,
    )?;
    assert_eq!(cloned.root(), destination);
    assert_eq!(fs::read(destination.join(".gpg-id"))?, b"ALICE\n");
    assert_eq!(cloned.remotes(), ["origin"]);
    assert_eq!(cloned.log(None)?.len(), 1);
    assert_eq!(cloned.log(None)?[0].message(), "Fetched initial store.");
    assert!(!destination.join(".gitattributes").exists());
    assert!(
        !temporary
            .path()
            .read_dir()?
            .filter_map(Result::ok)
            .any(|entry| entry
                .file_name()
                .to_string_lossy()
                .starts_with(".ironstorage-clone-"))
    );
    Ok(())
}

#[test]
fn branch_discovery_and_controlled_clone_stay_inside_rust() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let remote = &config.git_remotes()[0];
    let control = GitOperationControl::default();
    let branches = GitRepository::discover_remote_branches_with_transport(
        temporary.path(),
        identity(),
        remote,
        &Credentials,
        &CloningFetch,
        &control,
    )?;
    assert_eq!(branches, ["main"]);
    assert!(
        !temporary
            .path()
            .read_dir()?
            .filter_map(Result::ok)
            .any(|entry| {
                entry
                    .file_name()
                    .to_string_lossy()
                    .starts_with(".ironstorage-probe-")
            })
    );

    let destination = temporary.path().join("selected-branch");
    let cloned = GitRepository::clone_into_with_transport_controlled(
        &destination,
        identity(),
        remote,
        Some(&branches[0]),
        &Credentials,
        &CloningFetch,
        &control,
    )?;
    assert_eq!(cloned.current_branch()?, "main");
    assert_eq!(fs::read(destination.join(".gpg-id"))?, b"ALICE\n");
    Ok(())
}

#[test]
fn cancelled_clone_never_touches_an_existing_destination() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let destination = temporary.path().join("existing");
    fs::create_dir(&destination)?;
    fs::write(destination.join("keep"), b"unchanged")?;
    let control = GitOperationControl::default();
    control.cancel();
    let result = GitRepository::clone_into_with_transport_controlled(
        &destination,
        identity(),
        &config.git_remotes()[0],
        Some("main"),
        &Credentials,
        &CloningFetch,
        &control,
    );
    assert!(matches!(result, Err(GitError::Cancelled)));
    assert_eq!(fs::read(destination.join("keep"))?, b"unchanged");
    Ok(())
}

#[test]
fn clone_race_preserves_the_destination_and_removes_private_work() -> TestResult {
    let temporary = tempfile::tempdir()?;
    let config = remote_config(&temporary)?;
    let destination = temporary.path().join("concurrent");
    fs::create_dir(&destination)?;
    let result = GitRepository::clone_into_with_transport(
        &destination,
        identity(),
        &config.git_remotes()[0],
        &Credentials,
        &OccupyingFetch(destination.clone()),
    );
    assert!(matches!(result, Err(GitError::Io { .. })));
    assert_eq!(fs::read(destination.join("keep"))?, b"concurrent contents");
    assert!(
        !temporary
            .path()
            .read_dir()?
            .filter_map(Result::ok)
            .any(|entry| {
                entry
                    .file_name()
                    .to_string_lossy()
                    .starts_with(".ironstorage-clone-")
            })
    );
    Ok(())
}

#[test]
fn signed_commits_have_a_verifiable_ascii_armored_gpgsig() -> TestResult {
    let fixture = FixtureSet::load()?;
    let alice = fixture.key("alice")?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let signer = keys.resolve(&alice.primary_fingerprint)?;
    let temporary = tempfile::tempdir()?;
    let store = Repository::open(temporary.path())?;
    let git = GitRepository::init(&store, identity())?;
    fs::write(temporary.path().join("secret.gpg"), b"ciphertext")?;
    git.stage(&["secret.gpg".into()])?;
    git.commit_signed(
        "Add signed password.",
        &keys,
        &signer,
        &mut SigningSecret(alice.passphrase.as_bytes().to_vec()),
    )?;

    let repository = open_test_git(temporary.path())?;
    let head = repository.head_commit()?;
    let decoded = head.decode()?;
    let mut commit = decoded.into_owned()?;
    let signature_index = commit
        .extra_headers
        .iter()
        .position(|(name, _)| name.as_slice() == b"gpgsig")
        .expect("gpgsig header");
    let armor = commit.extra_headers.remove(signature_index).1;
    assert!(armor.starts_with(b"-----BEGIN PGP SIGNATURE-----"));
    let (signature, _) = DetachedSignature::from_armor_single(Cursor::new(armor))?;
    let mut signature_bytes = Vec::new();
    signature.to_writer(&mut signature_bytes)?;
    let mut unsigned = Vec::new();
    gix::objs::WriteTo::write_to(&commit, &mut unsigned)?;
    assert_eq!(
        keys.verify(
            &unsigned,
            &DetachedSignatureBytes::new(signature_bytes),
            std::slice::from_ref(&signer),
        )?,
        signer
    );
    Ok(())
}

#[test]
fn successful_storage_mutations_use_the_embedded_committer() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let mut git = GitRepository::init(&repository, identity())?;
    assert_eq!(
        fs::read(store.path().join(".gitattributes"))?,
        b"*.gpg diff=gpg\n"
    );
    let initialized = git.log(None)?;
    assert_eq!(initialized.len(), 2);
    assert_eq!(
        initialized[0].message(),
        "Configure git repository for gpg file diff."
    );
    assert_eq!(
        initialized[1].message(),
        "Add current contents of password store."
    );
    fs::write(store.path().join("unrelated.gpg"), b"separately staged")?;
    git.stage(&["unrelated.gpg".into()])?;
    let writer = VaultWriter::new(&repository, &keys);
    writer.insert(
        &InsertRequest {
            entry: "automatic/entry".to_owned(),
            input: InsertInput::EchoedLine,
            force: false,
        },
        InsertContent::echoed(b"generated secret".to_vec())?,
        OverwriteDecision::Decline,
        None,
        &mut git,
    )?;
    let log = git.log(None)?;
    assert_eq!(log.len(), 3);
    assert_eq!(
        log[0].message(),
        "Add given password for automatic/entry to store."
    );
    let embedded = open_test_git(store.path())?;
    let head = embedded.head_commit()?;
    let committed = head.tree()?;
    assert!(
        committed
            .lookup_entry_by_path("automatic/entry.gpg")?
            .is_some()
    );
    assert!(committed.lookup_entry_by_path("unrelated.gpg")?.is_none());
    let status = git.status()?;
    assert_eq!(status.staged().len(), 1, "{status:?}");
    assert_eq!(status.staged()[0].path(), Path::new("unrelated.gpg"));
    assert_eq!(status.staged()[0].kind(), GitChangeKind::Added);
    Ok(())
}

fn reset_head_and_index(git: &GitRepository, root: &Path, id: &str, contents: &[u8]) -> TestResult {
    let repository = open_test_git(root)?;
    let id = gix::hash::ObjectId::from_hex(id.as_bytes())?;
    repository
        .head_ref()?
        .expect("born HEAD")
        .set_target_id(id, "test reset")?;
    fs::write(root.join("secret.gpg"), contents)?;
    git.stage(&["secret.gpg".into()])?;
    Ok(())
}

fn set_remote_tracking(root: &Path, id: &str) -> TestResult {
    let repository = open_test_git(root)?;
    let id = gix::hash::ObjectId::from_hex(id.as_bytes())?;
    if let Some(existing) = repository.try_find_reference("refs/remotes/origin/main")? {
        existing.delete()?;
    }
    repository.reference(
        "refs/remotes/origin/main",
        id,
        gix::refs::transaction::PreviousValue::MustNotExist,
        "test remote update",
    )?;
    Ok(())
}

fn reset_to_unborn(root: &Path) -> TestResult {
    let repository = open_test_git(root)?;
    repository.find_reference("refs/heads/main")?.delete()?;
    let mut index = repository.index_from_tree(&repository.empty_tree().id)?;
    index.write(Default::default())?;
    fs::remove_file(root.join(".gpg-id"))?;
    Ok(())
}

fn open_test_git(root: &Path) -> Result<gix::Repository, Box<dyn Error>> {
    Ok(gix::open_opts(
        root,
        gix::open::Options::isolated().config_overrides([
            "user.name=Test".to_owned(),
            "user.email=test@ironstorage.invalid".to_owned(),
        ]),
    )?)
}

fn packet(data: &[u8]) -> Vec<u8> {
    let mut output = format!("{:04x}", data.len() + 4).into_bytes();
    output.extend_from_slice(data);
    output
}
