//! Storage-owned OpenPGP key transfer for native mobile QR adapters.

use std::{collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path};

use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
use sha2::{Digest as _, Sha256};

use crate::{
    config::{Config, ConfigError},
    crypto::{CryptoError, KeyInfo, KeyStore},
    presentation::{QrError, QrMatrix},
    repository::SecretBytes,
};

const FRAME_PREFIX: &str = "ISKT1";
const FRAME_CHUNK_BYTES: usize = 1_600;
const MAX_TRANSFER_FRAMES: usize = 11_000;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileKeyTransferKind {
    Public,
    Private,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileKeyTransferKey {
    fingerprint: String,
    title: String,
    detail: String,
    kind: MobileKeyTransferKind,
    requires_passphrase: bool,
}

impl MobileKeyTransferKey {
    pub fn fingerprint(&self) -> &str {
        &self.fingerprint
    }

    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn detail(&self) -> &str {
        &self.detail
    }

    pub fn kind(&self) -> MobileKeyTransferKind {
        self.kind
    }

    pub fn requires_passphrase(&self) -> bool {
        self.requires_passphrase
    }
}

pub struct MobileKeyTransferFrame {
    sequence: usize,
    total: usize,
    payload: SecretBytes,
    matrix: QrMatrix,
}

impl MobileKeyTransferFrame {
    pub fn sequence(&self) -> usize {
        self.sequence
    }

    pub fn total(&self) -> usize {
        self.total
    }

    pub fn payload(&self) -> &[u8] {
        self.payload.expose()
    }

    pub fn matrix(&self) -> &QrMatrix {
        &self.matrix
    }
}

pub struct MobileKeyTransferExport {
    key: MobileKeyTransferKey,
    frames: Vec<MobileKeyTransferFrame>,
}

impl MobileKeyTransferExport {
    pub fn key(&self) -> &MobileKeyTransferKey {
        &self.key
    }

    pub fn frames(&self) -> &[MobileKeyTransferFrame] {
        &self.frames
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileKeyTransferProgress {
    received: usize,
    total: usize,
    duplicate: bool,
    key: Option<MobileKeyTransferKey>,
}

impl MobileKeyTransferProgress {
    pub fn received(&self) -> usize {
        self.received
    }

    pub fn total(&self) -> usize {
        self.total
    }

    pub fn duplicate(&self) -> bool {
        self.duplicate
    }

    pub fn key(&self) -> Option<&MobileKeyTransferKey> {
        self.key.as_ref()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileKeyTransferOutcome {
    title: String,
    detail: String,
}

impl MobileKeyTransferOutcome {
    pub fn title(&self) -> &str {
        &self.title
    }

    pub fn detail(&self) -> &str {
        &self.detail
    }
}

pub struct MobileKeyTransferService {
    config: Config,
    keys: KeyStore,
}

impl MobileKeyTransferService {
    pub fn load() -> Result<Self, MobileKeyTransferError> {
        let config = Config::load(None)?;
        let keys = KeyStore::load(config.key_material())?;
        Ok(Self { config, keys })
    }

    pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
        self.keys.infos().map(|key| transfer_key(&key)).collect()
    }

    pub fn export(
        &self,
        fingerprint: &str,
        kind: MobileKeyTransferKind,
        passphrase: Option<SecretBytes>,
    ) -> Result<MobileKeyTransferExport, MobileKeyTransferError> {
        let include_secret = kind == MobileKeyTransferKind::Private;
        let armor = self
            .keys
            .export_armored(fingerprint, include_secret, passphrase.as_ref())?;
        let info = self
            .keys
            .infos()
            .find(|key| key.fingerprint().as_str() == fingerprint)
            .ok_or(MobileKeyTransferError::KeyUnavailable)?;
        let frames = encode_frames(armor)?;
        Ok(MobileKeyTransferExport {
            key: transfer_key_with_kind(&info, kind),
            frames,
        })
    }

    pub fn importer(&self) -> MobileKeyTransferImport {
        MobileKeyTransferImport::new(self.config.clone())
    }
}

pub struct MobileKeyTransferImport {
    config: Config,
    digest: Option<String>,
    total: Option<usize>,
    chunks: BTreeMap<usize, Vec<u8>>,
    complete: Option<SecretBytes>,
    key: Option<MobileKeyTransferKey>,
}

impl MobileKeyTransferImport {
    fn new(config: Config) -> Self {
        Self {
            config,
            digest: None,
            total: None,
            chunks: BTreeMap::new(),
            complete: None,
            key: None,
        }
    }

    pub fn add_frame(
        &mut self,
        payload: SecretBytes,
    ) -> Result<MobileKeyTransferProgress, MobileKeyTransferError> {
        if self.complete.is_some() {
            return Err(MobileKeyTransferError::AlreadyComplete);
        }
        if payload.expose().starts_with(b"-----BEGIN PGP ") {
            if self.total.is_some() {
                return Err(MobileKeyTransferError::MixedTransfers);
            }
            self.finish(payload)?;
            return Ok(self.progress(false));
        }

        let text = std::str::from_utf8(payload.expose())
            .map_err(|_| MobileKeyTransferError::InvalidFrame)?;
        let mut fields = text.splitn(5, ':');
        if fields.next() != Some(FRAME_PREFIX) {
            return Err(MobileKeyTransferError::InvalidFrame);
        }
        let digest = fields.next().ok_or(MobileKeyTransferError::InvalidFrame)?;
        if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
            return Err(MobileKeyTransferError::InvalidFrame);
        }
        let sequence = parse_positive(fields.next())?;
        let total = parse_positive(fields.next())?;
        if sequence > total || total > MAX_TRANSFER_FRAMES {
            return Err(MobileKeyTransferError::InvalidFrame);
        }
        let chunk = BASE64URL_NOPAD
            .decode(
                fields
                    .next()
                    .ok_or(MobileKeyTransferError::InvalidFrame)?
                    .as_bytes(),
            )
            .map_err(|_| MobileKeyTransferError::InvalidFrame)?;
        if chunk.is_empty() || chunk.len() > FRAME_CHUNK_BYTES {
            return Err(MobileKeyTransferError::InvalidFrame);
        }

        if self.digest.as_deref().is_some_and(|value| value != digest)
            || self.total.is_some_and(|value| value != total)
        {
            return Err(MobileKeyTransferError::MixedTransfers);
        }
        self.digest
            .get_or_insert_with(|| digest.to_ascii_lowercase());
        self.total.get_or_insert(total);
        if let Some(existing) = self.chunks.get(&sequence) {
            if existing == &chunk {
                return Ok(self.progress(true));
            }
            return Err(MobileKeyTransferError::ConflictingFrame);
        }
        self.chunks.insert(sequence, chunk);
        if self.chunks.len() == total {
            let mut armor = Vec::new();
            for sequence in 1..=total {
                armor.extend(
                    self.chunks
                        .get(&sequence)
                        .ok_or(MobileKeyTransferError::IncompleteTransfer)?,
                );
            }
            let actual = HEXLOWER.encode(&Sha256::digest(&armor));
            if self.digest.as_deref() != Some(actual.as_str()) {
                return Err(MobileKeyTransferError::ChecksumMismatch);
            }
            self.finish(SecretBytes::new(armor))?;
        }
        Ok(self.progress(false))
    }

    pub fn import(
        &mut self,
        passphrase: Option<SecretBytes>,
        make_default: bool,
    ) -> Result<MobileKeyTransferOutcome, MobileKeyTransferError> {
        let armor = self
            .complete
            .as_ref()
            .ok_or(MobileKeyTransferError::IncompleteTransfer)?;
        let key = self
            .key
            .as_ref()
            .ok_or(MobileKeyTransferError::IncompleteTransfer)?;
        let mut imported = KeyStore::new();
        imported.import(armor.expose())?;
        if key.kind == MobileKeyTransferKind::Private {
            imported.validate_secret_passphrase(&key.fingerprint, passphrase.as_ref())?;
        }
        if make_default && key.kind != MobileKeyTransferKind::Private {
            return Err(MobileKeyTransferError::PublicDefault);
        }

        let existing = KeyStore::load(self.config.key_material())?;
        if let Some(found) = existing
            .infos()
            .find(|info| info.fingerprint().as_str() == key.fingerprint)
            && (found.has_secret() || key.kind == MobileKeyTransferKind::Public)
        {
            return Err(MobileKeyTransferError::DuplicateKey);
        }

        persist_key_material(self.config.key_material(), key, armor.expose())?;
        if make_default {
            let mut settings = self.config.settings();
            settings.set_default_key(key.fingerprint.clone());
            self.config.with_settings(settings)?.persist()?;
        }
        let kind = if key.kind == MobileKeyTransferKind::Private {
            "private"
        } else {
            "public"
        };
        Ok(MobileKeyTransferOutcome {
            title: "GPG Key Imported".to_owned(),
            detail: format!("Imported the {kind} key {}.", key.fingerprint),
        })
    }

    fn finish(&mut self, armor: SecretBytes) -> Result<(), MobileKeyTransferError> {
        validate_transfer_armor(armor.expose())?;
        let mut store = KeyStore::new();
        let infos = store.import(armor.expose())?;
        if infos.len() != 1 {
            return Err(MobileKeyTransferError::MismatchedKeys);
        }
        self.key = infos.first().map(transfer_key);
        self.complete = Some(armor);
        Ok(())
    }

    fn progress(&self, duplicate: bool) -> MobileKeyTransferProgress {
        MobileKeyTransferProgress {
            received: self.chunks.len().max(usize::from(self.complete.is_some())),
            total: self.total.unwrap_or(1),
            duplicate,
            key: self.key.clone(),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MobileKeyTransferError {
    Configuration,
    KeyUnavailable,
    InvalidFrame,
    MixedTransfers,
    ConflictingFrame,
    IncompleteTransfer,
    AlreadyComplete,
    ChecksumMismatch,
    MismatchedKeys,
    DuplicateKey,
    PublicDefault,
    InvalidKeyMaterial,
    IncorrectPassphrase,
    QrPayload,
    Write,
}

impl fmt::Display for MobileKeyTransferError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Configuration => "the IronStorage configuration is unavailable",
            Self::KeyUnavailable => "the selected GPG key is unavailable",
            Self::InvalidFrame => "this is not a valid IronStorage key-transfer QR code",
            Self::MixedTransfers => "the scanned QR codes belong to different key transfers",
            Self::ConflictingFrame => "a scanned QR frame conflicts with an earlier frame",
            Self::IncompleteTransfer => {
                "more QR frames are required before this key can be imported"
            }
            Self::AlreadyComplete => "this key transfer is already complete",
            Self::ChecksumMismatch => {
                "the reconstructed key transfer did not pass its integrity check"
            }
            Self::MismatchedKeys => "the transfer contains multiple or mismatched GPG keys",
            Self::DuplicateKey => "this GPG key is already installed",
            Self::PublicDefault => "a public-only key cannot become the default GPG key",
            Self::InvalidKeyMaterial => "the GPG key material is invalid",
            Self::IncorrectPassphrase => "the GPG key passphrase is incorrect",
            Self::QrPayload => "the GPG key could not be represented as QR codes",
            Self::Write => "the GPG key could not be stored safely",
        })
    }
}

impl Error for MobileKeyTransferError {}

impl From<ConfigError> for MobileKeyTransferError {
    fn from(_: ConfigError) -> Self {
        Self::Configuration
    }
}

impl From<CryptoError> for MobileKeyTransferError {
    fn from(error: CryptoError) -> Self {
        match error {
            CryptoError::InvalidKeyPassphrase => Self::IncorrectPassphrase,
            CryptoError::MissingIdentity { .. } | CryptoError::MissingSecretKey => {
                Self::KeyUnavailable
            }
            _ => Self::InvalidKeyMaterial,
        }
    }
}

impl From<QrError> for MobileKeyTransferError {
    fn from(_: QrError) -> Self {
        Self::QrPayload
    }
}

fn transfer_key(info: &KeyInfo) -> MobileKeyTransferKey {
    transfer_key_with_kind(
        info,
        if info.has_secret() {
            MobileKeyTransferKind::Private
        } else {
            MobileKeyTransferKind::Public
        },
    )
}

fn transfer_key_with_kind(info: &KeyInfo, kind: MobileKeyTransferKind) -> MobileKeyTransferKey {
    let title = info
        .user_ids()
        .first()
        .cloned()
        .unwrap_or_else(|| format!("GPG key {}", info.key_id()));
    MobileKeyTransferKey {
        fingerprint: info.fingerprint().as_str().to_owned(),
        title,
        detail: format!("Fingerprint {}", info.fingerprint()),
        kind,
        requires_passphrase: info.requires_passphrase(),
    }
}

fn encode_frames(
    armor: SecretBytes,
) -> Result<Vec<MobileKeyTransferFrame>, MobileKeyTransferError> {
    if let Ok(matrix) = QrMatrix::encode(&armor) {
        return Ok(vec![MobileKeyTransferFrame {
            sequence: 1,
            total: 1,
            payload: armor,
            matrix,
        }]);
    }
    encode_multipart(armor, FRAME_CHUNK_BYTES)
}

fn encode_multipart(
    armor: SecretBytes,
    chunk_bytes: usize,
) -> Result<Vec<MobileKeyTransferFrame>, MobileKeyTransferError> {
    if chunk_bytes == 0 {
        return Err(MobileKeyTransferError::QrPayload);
    }
    let digest = HEXLOWER.encode(&Sha256::digest(armor.expose()));
    let total = armor.expose().len().div_ceil(chunk_bytes);
    if total == 0 || total > MAX_TRANSFER_FRAMES {
        return Err(MobileKeyTransferError::QrPayload);
    }
    let mut frames = Vec::with_capacity(total);
    for (index, chunk) in armor.expose().chunks(chunk_bytes).enumerate() {
        let payload = SecretBytes::new(
            format!(
                "{FRAME_PREFIX}:{digest}:{}:{total}:{}",
                index + 1,
                BASE64URL_NOPAD.encode(chunk)
            )
            .into_bytes(),
        );
        let matrix = QrMatrix::encode(&payload)?;
        frames.push(MobileKeyTransferFrame {
            sequence: index + 1,
            total,
            payload,
            matrix,
        });
    }
    Ok(frames)
}

fn parse_positive(value: Option<&str>) -> Result<usize, MobileKeyTransferError> {
    value
        .ok_or(MobileKeyTransferError::InvalidFrame)?
        .parse::<usize>()
        .ok()
        .filter(|value| *value > 0)
        .ok_or(MobileKeyTransferError::InvalidFrame)
}

fn validate_transfer_armor(bytes: &[u8]) -> Result<(), MobileKeyTransferError> {
    let text =
        std::str::from_utf8(bytes).map_err(|_| MobileKeyTransferError::InvalidKeyMaterial)?;
    let end = if text.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") {
        "-----END PGP PUBLIC KEY BLOCK-----"
    } else if text.starts_with("-----BEGIN PGP PRIVATE KEY BLOCK-----") {
        "-----END PGP PRIVATE KEY BLOCK-----"
    } else {
        return Err(MobileKeyTransferError::InvalidKeyMaterial);
    };
    if text.matches("-----BEGIN PGP ").count() != 1 {
        return Err(MobileKeyTransferError::MismatchedKeys);
    }
    let end_offset = text
        .find(end)
        .map(|offset| offset + end.len())
        .ok_or(MobileKeyTransferError::InvalidKeyMaterial)?;
    if !text[end_offset..].trim().is_empty() {
        return Err(MobileKeyTransferError::InvalidKeyMaterial);
    }
    Ok(())
}

fn persist_key_material(
    configured: &Path,
    key: &MobileKeyTransferKey,
    armor: &[u8],
) -> Result<(), MobileKeyTransferError> {
    if configured.is_dir() {
        let suffix = if key.kind == MobileKeyTransferKind::Private {
            "secret"
        } else {
            "public"
        };
        let name = format!("{}-{suffix}.asc", key.fingerprint.to_ascii_lowercase());
        atomic_replace(configured, Path::new(&name), armor)
    } else {
        let existing = fs::read(configured).map_err(|_| MobileKeyTransferError::Write)?;
        let mut combined = Vec::with_capacity(existing.len() + armor.len() + 1);
        combined.extend_from_slice(&existing);
        if !combined.ends_with(b"\n") {
            combined.push(b'\n');
        }
        combined.extend_from_slice(armor);
        let parent = configured.parent().ok_or(MobileKeyTransferError::Write)?;
        let name = configured
            .file_name()
            .ok_or(MobileKeyTransferError::Write)?;
        atomic_replace(parent, Path::new(name), &combined)
    }
}

fn atomic_replace(
    parent: &Path,
    name: &Path,
    contents: &[u8],
) -> Result<(), MobileKeyTransferError> {
    let directory = Dir::open_ambient_dir(parent, ambient_authority())
        .map_err(|_| MobileKeyTransferError::Write)?;
    if let Ok(metadata) = directory.symlink_metadata(name)
        && (metadata.file_type().is_symlink() || !metadata.is_file())
    {
        return Err(MobileKeyTransferError::Write);
    }
    let mut temporary = TempFile::new(&directory).map_err(|_| MobileKeyTransferError::Write)?;
    set_private_permissions(&temporary)?;
    temporary
        .write_all(contents)
        .and_then(|()| temporary.as_file().sync_all())
        .and_then(|()| temporary.replace(name))
        .and_then(|()| directory.open(".").and_then(|file| file.sync_all()))
        .map_err(|_| MobileKeyTransferError::Write)
}

#[cfg(unix)]
fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), MobileKeyTransferError> {
    use cap_std::fs::{Permissions, PermissionsExt as _};

    temporary
        .as_file()
        .set_permissions(Permissions::from_mode(0o600))
        .map_err(|_| MobileKeyTransferError::Write)
}

#[cfg(not(unix))]
fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), MobileKeyTransferError> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{
        MobileKeyTransferError, MobileKeyTransferImport, MobileKeyTransferKind,
        MobileKeyTransferService, encode_multipart,
    };
    use crate::{config::Config, crypto::KeyStore, repository::SecretBytes};
    use tempfile::tempdir;

    const ALICE_PUBLIC: &[u8] =
        include_bytes!("../tests/fixtures/compatibility/keys/alice-public.asc");
    const ALICE_SECRET: &[u8] =
        include_bytes!("../tests/fixtures/compatibility/keys/alice-secret.asc");
    const ALICE_FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";

    #[test]
    fn multipart_frames_reconstruct_exact_armor_out_of_order()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = configured_public_key()?;
        let mut importer = MobileKeyTransferImport::new(fixture.config);
        let mut frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
        frames.reverse();
        for frame in frames {
            importer.add_frame(SecretBytes::new(frame.payload().to_vec()))?;
        }
        assert_eq!(
            importer.complete.as_ref().map(SecretBytes::expose),
            Some(ALICE_SECRET)
        );
        assert_eq!(
            importer.key.as_ref().map(|key| key.kind()),
            Some(MobileKeyTransferKind::Private)
        );
        Ok(())
    }

    #[test]
    fn single_frame_export_validates_private_passphrase_and_reconstructs_exactly()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = configured_secret_key()?;
        let service = MobileKeyTransferService {
            config: fixture.config.clone(),
            keys: KeyStore::load(&fixture.key_path)?,
        };
        assert!(matches!(
            service.export(
                ALICE_FINGERPRINT,
                MobileKeyTransferKind::Private,
                Some(SecretBytes::new(b"wrong".to_vec())),
            ),
            Err(MobileKeyTransferError::IncorrectPassphrase)
        ));
        let exported = service.export(
            ALICE_FINGERPRINT,
            MobileKeyTransferKind::Private,
            Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
        )?;
        assert_eq!(exported.frames().len(), 1);
        let expected = exported.frames()[0].payload().to_vec();
        let mut importer = service.importer();
        let progress = importer.add_frame(SecretBytes::new(expected.clone()))?;
        assert!(progress.key().is_some());
        assert_eq!(
            importer.complete.as_ref().map(SecretBytes::expose),
            Some(expected.as_slice())
        );
        Ok(())
    }

    #[test]
    fn mismatched_public_keys_are_rejected_as_one_transfer()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = configured_public_key()?;
        let mut importer = MobileKeyTransferImport::new(fixture.config);
        let mut mismatched = ALICE_PUBLIC.to_vec();
        mismatched.extend_from_slice(include_bytes!(
            "../tests/fixtures/compatibility/keys/bob-public.asc"
        ));
        assert_eq!(
            importer.add_frame(SecretBytes::new(mismatched)),
            Err(MobileKeyTransferError::MismatchedKeys)
        );
        Ok(())
    }

    #[test]
    fn invalid_duplicate_and_incomplete_frames_do_not_replace_keys()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = configured_public_key()?;
        let before = std::fs::read(fixture.key_path.join("alice-public.asc"))?;
        let mut importer = MobileKeyTransferImport::new(fixture.config);
        let frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
        let first = frames[0].payload();
        let progress = importer.add_frame(SecretBytes::new(first.to_vec()))?;
        assert!(
            importer
                .add_frame(SecretBytes::new(first.to_vec()))?
                .duplicate()
        );
        assert!(progress.received() < progress.total());
        assert_eq!(
            importer.import(None, false),
            Err(MobileKeyTransferError::IncompleteTransfer)
        );
        assert_eq!(
            std::fs::read(fixture.key_path.join("alice-public.asc"))?,
            before
        );
        Ok(())
    }

    #[test]
    fn private_import_validates_passphrase_before_atomic_persistence()
    -> Result<(), Box<dyn std::error::Error>> {
        let fixture = configured_public_key()?;
        let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
        importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
        assert_eq!(
            importer.import(Some(SecretBytes::new(b"wrong".to_vec())), true),
            Err(MobileKeyTransferError::IncorrectPassphrase)
        );
        assert_eq!(std::fs::read_dir(&fixture.key_path)?.count(), 1);
        importer.import(
            Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
            true,
        )?;
        let keys = KeyStore::load(&fixture.key_path)?;
        assert!(keys.infos().any(|key| key.has_secret()));
        assert_eq!(
            Config::load(Some(fixture.config.source()))?
                .default_key()
                .as_str(),
            ALICE_FINGERPRINT
        );
        Ok(())
    }

    struct Fixture {
        _root: tempfile::TempDir,
        config: Config,
        key_path: std::path::PathBuf,
    }

    fn configured_public_key() -> Result<Fixture, Box<dyn std::error::Error>> {
        configured_key("alice-public.asc", ALICE_PUBLIC)
    }

    fn configured_secret_key() -> Result<Fixture, Box<dyn std::error::Error>> {
        configured_key("alice-secret.asc", ALICE_SECRET)
    }

    fn configured_key(name: &str, bytes: &[u8]) -> Result<Fixture, Box<dyn std::error::Error>> {
        let root = tempdir()?;
        let vault = root.path().join("vault");
        let keys = root.path().join("keys");
        std::fs::create_dir(&vault)?;
        std::fs::create_dir(&keys)?;
        std::fs::write(keys.join(name), bytes)?;
        let config_path = root.path().join("config.toml");
        std::fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
                vault, ALICE_FINGERPRINT, keys
            ),
        )?;
        let config = Config::load(Some(&config_path))?;
        Ok(Fixture {
            _root: root,
            config,
            key_path: keys,
        })
    }
}
