//! Import of destructured [OpenPGP certificates] as [VOA] verifiers.
//!
//! Destructured [OpenPGP certificates] are represented by (binary or ASCII-armored) [OpenPGP
//! packet] data in separate files.
//!
//! # Note
//!
//! Destructured [OpenPGP certificates] are a non-standardized format.
//!
//! # Formats
//!
//! This module allows reading [OpenPGP certificates] from the following set of directory
//! structures.
//!
//! # Flat structure
//!
//! A flat structure can be created by splitting an OpenPGP certificate using specialised tooling
//! such as [`rpacket`]:
//!
//! ```bash
//! rpacket split < cert.pgp
//! ```
//!
//! This may create output similar to the following:
//!
//! ```text
//! .
//! ├── 000000-PublicKey
//! ├── 000001-UserId
//! ├── 000002-Signature
//! ├── 000003-Signature
//! ├── 000004-Signature
//! ├── 000005-Signature
//! ├── 000006-Signature
//! ├── 000007-Signature
//! ├── 000008-Signature
//! ├── 000009-Signature
//! ├── 000010-PublicSubkey
//! ├── 000011-Signature
//! ├── 000012-PublicSubkey
//! ├── 000013-Signature
//! ├── 000014-PublicSubkey
//! └── 000015-Signature
//! ```
//!
//! Here, each file contains raw [OpenPGP packet] data.
//! The concatenation of all files in sequence represents a valid OpenPGP certificate, e.g.
//!
//! ```bash
//! cat 0000* > cert-concant.pgp
//! ```
//!
//! # Arch Linux keyring structure
//!
//! The [archlinux-keyring] project chose a more fine grained approach, that is based on a custom
//! directory structure. Here, files containing raw ASCII-armored [OpenPGP packet] data are grouped
//! by their specific use in an OpenPGP certificate.
//!
//! A single top-level file contains the primary component key, named after its [OpenPGP
//! fingerprint] (e.g. `F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`).
//!
//! The following directories are used to group specific [OpenPGP packet] data:
//!
//! - `revocation`: If it exists, contains a file containing [Key Revocation Signature] data, named
//!   after the [OpenPGP fingerprint] of the primary component key (e.g.
//!   `revocation/F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`).
//! - `directkey`: If it exists, contains a directory structure in which files containing [Direct
//!   Key Signature] data reside. Individual files are located in a directory that reflects the
//!   [OpenPGP fingerprint] of the targeted component key and are named after their specific
//!   creation time (e.g.
//!   `directkey/certification/F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15/2024-06-23_12-55-20.asc`)
//! - `uid`: If it exists, contains a directory structure for each [User ID] or [User Attribute]
//!   packet of the certificate. Here, [User ID]s are represented by directories named after their
//!   string representation, with unusable characters replaced and an additional unique identifier
//!   appended to prevent collision (e.g. `John Doe <jdoe@example.org>` ->
//!   `uid/John_Doe__jdoe@example.org_d2ad250f`). Each [User ID] directory contains a top-level
//!   file, which represents the [User ID] packet (e.g.
//!   `uid/John_Doe__jdoe@example.org_d2ad250f/John_Doe__jdoe@example.org_d2ad250f.asc`). Further,
//!   each such directory contains a `certification` and may contain a `revocation` directory. The
//!   `certification` directory may contain User ID binding signatures and third-party
//!   certifications (e.g. `uid/John_Doe__jdoe@example.org_d2ad250f/certification/
//!   F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`). The `revocation` directory may contain User ID
//!   revocation signatures or third-party certification revocation signatures (e.g.
//!   `uid/John_Doe__jdoe@example.org_d2ad250f/revocation/
//!   F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`).
//! - `subkey`: If it exists, contains a directory structure for each subkey component key bound to
//!   the primary component key of the certificate. A top-level directory is named after the
//!   [OpenPGP fingerprint] of the component key (e.g.
//!   `subkey/E242ED3BFFCCDF271B7FBAF34ED72D089537B42F/`). Each top-level directory contains a file
//!   containing [Public Subkey] data (e.g.
//!   `subkey/E242ED3BFFCCDF271B7FBAF34ED72D089537B42F/E242ED3BFFCCDF271B7FBAF34ED72D089537B42F.
//!   asc`). Further, each such directory contains a `certification` and may contain a `revocation`
//!   directory. The `certification` directory contains files containing [Subkey Binding Signature]
//!   data, named after the [OpenPGP fingerprint] of the issuing key (e.g.
//!   `subkey/E242ED3BFFCCDF271B7FBAF34ED72D089537B42F/certification/
//!   F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`). The `revocation` directory may contain files
//!   containing [Subkey Revocation Signature] data, named after the [OpenPGP fingerprint] of the
//!   issuing key (e.g. `subkey/E242ED3BFFCCDF271B7FBAF34ED72D089537B42F/revocation/
//!   F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc`).
//!
//! The following example illustrates a destructured OpenPGP certificate using the
//! [archlinux-keyring] specific directory format:
//!
//! ```text
//! .
//! ├── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
//! ├── subkey
//! │   ├── E242ED3BFFCCDF271B7FBAF34ED72D089537B42F
//! │   │   ├── E242ED3BFFCCDF271B7FBAF34ED72D089537B42F.asc
//! │   │   └── certification
//! │   │       └── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
//! │   ├── D3B0F7C0B825ECBB0F0D7398072947E7B1537B6F
//! │   │   ├── D3B0F7C0B825ECBB0F0D7398072947E7B1537B6F.asc
//! │   │   └── certification
//! │   │       └── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
//! │   └── 6EADEAC2DADE6347E87C0D24FD455FEFFA7069F0
//! │       ├── 6EADEAC2DADE6347E87C0D24FD455FEFFA7069F0.asc
//! │       └── certification
//! │           └── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
//! └── uid
//!     └── John_Doe__jdoe@example.org_d2ad250f
//!         ├── John_Doe__jdoe@example.org_d2ad250f.asc
//!         └── certification
//!             ├── B787A81C32997FD39A5F4C0188363902D3586E7B.asc
//!             ├── 2072A695613E5103D9AC03C2885C5E2656CB5FF0.asc
//!             ├── 68D61AF364B99AD0226A9C8859F18BF95A99BCE9.asc
//!             ├── 033DB9A2637803F63BDA651106B2C4BEF184C21D.asc
//!             ├── 868672B9CDB0BF449BF3782CFDA1DBE372838AA3.asc
//!             ├── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
//!             ├── 98EECC29ABC53C31B0DA5C85CB26CE720C7FF763.asc
//!             └── 52428846EFFD79371A81D6C82D00FBFED9C654F3.asc
//! ```
//!
//! [Direct Key Signature]: https://www.rfc-editor.org/rfc/rfc9580#name-direct-key-signature-type-i
//! [Key Revocation Signature]: https://www.rfc-editor.org/rfc/rfc9580#name-key-revocation-signature-ty
//! [OpenPGP certificates]: https://openpgp.dev/book/certificates.html
//! [OpenPGP fingerprint]: https://openpgp.dev/book/certificates.html#fingerprint
//! [OpenPGP packet]: https://openpgp.dev/book/zoom/certificates.html
//! [Public Subkey]: https://www.rfc-editor.org/rfc/rfc9580#name-public-subkey-packet-type-i
//! [Subkey Binding Signature]: https://www.rfc-editor.org/rfc/rfc9580#name-subkey-binding-signature-ty
//! [Subkey Revocation Signature]: https://www.rfc-editor.org/rfc/rfc9580#name-subkey-revocation-signature
//! [User ID]: https://www.rfc-editor.org/rfc/rfc9580#uid
//! [User Attribute]: https://www.rfc-editor.org/rfc/rfc9580#name-user-attribute-packet-type-
//! [VOA]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/
//! [`rpacket`]: https://codeberg.org/heiko/rpacket
//! [archlinux-keyring]: https://gitlab.archlinux.org/archlinux/archlinux-keyring/

pub mod error;

use std::{
    fs::{File, Metadata},
    io::{BufRead, BufReader, Read},
    path::{Path, PathBuf},
};

use error::Error;
use log::{debug, trace};
use pgp::{
    armor::Dearmor,
    composed::{Deserializable, SignedPublicKey},
    packet::{Packet, PacketParser, PacketTrait},
    types::KeyDetails,
};

use crate::import::OpenPgpImport;

/// The name of a directory used for OpenPGP packets that serve as certification.
pub(crate) const CERTIFICATION_DIR: &str = "certification";
/// The name of a directory used for OpenPGP packets that serve as revocation.
pub(crate) const REVOCATION_DIR: &str = "revocation";
/// The name of a directory used for OpenPGP packets that serve as direct key signatures.
pub(crate) const DIRECTKEY_DIR: &str = "directkey";
/// The name of a directory containing directory structures with OpenPGP packet files for User IDs.
pub(crate) const UID_DIR: &str = "uid";
/// The name of a directory containing directory structures with OpenPGP packet files for subkeys.
pub(crate) const SUBKEY_DIR: &str = "subkey";

/// Reads a directory `path` and returns its entries as a sorted list of paths.
///
/// # Errors
///
/// Returns an error if
///
/// - `path` cannot be read using [`Path::read_dir`],
/// - or one of the directory entries in `path` is not readable.
fn read_dir_as_sorted_paths(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, crate::Error> {
    let dir = path.as_ref();
    let read_dir = dir.read_dir().map_err(|source| crate::Error::IoPath {
        path: dir.to_path_buf(),
        context: "reading entries of a directory",
        source,
    })?;

    let paths = {
        let mut paths: Vec<PathBuf> = Vec::new();

        for dir_entry in read_dir {
            let entry = dir_entry.map_err(|source| crate::Error::IoPath {
                path: dir.to_path_buf(),
                context: "reading entry in directory",
                source,
            })?;
            paths.push(entry.path());
        }

        // Ensure a stable sorted list of paths.
        // Some filesystems are not sorted by default.
        paths.sort();

        paths
    };

    Ok(paths)
}

/// Retrieves [`Metadata`] for a `path` and maps [`std::io::Error`] to [`crate::Error`].
fn path_metadata(path: impl AsRef<Path>) -> Result<Metadata, crate::Error> {
    let path = path.as_ref();
    path.metadata().map_err(|source| crate::Error::IoPath {
        path: path.to_path_buf(),
        context: "retrieving metadata for a path",
        source,
    })
}

/// Collects all regular files in a directory `path`.
///
/// # Errors
///
/// Returns an error if
///
/// - `path` is not a directory,
/// - an entry in `path` cannot be read,
/// - metadata for an entry in `path` cannot be retrieved,
/// - or any path in `path` does not represent a regular file.
fn files_in_dir(path: impl AsRef<Path>, list: &mut Vec<PathBuf>) -> Result<(), crate::Error> {
    let dir = path.as_ref();
    let paths = read_dir_as_sorted_paths(dir)?;

    for path in paths {
        let metadata = path_metadata(&path)?;

        if !metadata.is_file() {
            return Err(crate::import::Error::DestructuredImport(
                Error::DirMustContainRegularFiles {
                    dir: dir.to_path_buf(),
                    path: path.clone(),
                },
            )
            .into());
        }

        list.push(path.to_path_buf());
    }

    Ok(())
}

/// Creates a list of component directories from a directory.
///
/// Component directories may be those for OpenPGP User IDs or OpenPGP subkeys.
///
/// # Errors
///
/// Returns an error if
///
/// - `path` is not a directory,
/// - an entry in `path` cannot be read,
/// - metadata for an entry in `path` cannot be retrieved,
/// - any path in `path` does not represent a directory,
/// - or any path in `path` cannot be turned into a [`ComponentDir`].
fn component_dirs_from_dir(path: impl AsRef<Path>) -> Result<Vec<ComponentDir>, crate::Error> {
    let dir = path.as_ref();
    let paths = read_dir_as_sorted_paths(dir)?;
    let mut dirs = Vec::new();

    for path in paths {
        let metadata = path_metadata(&path)?;

        if !metadata.is_dir() {
            return Err(
                crate::import::Error::DestructuredImport(Error::DirMustContainDirs {
                    dir: dir.to_path_buf(),
                    path: path.clone(),
                })
                .into(),
            );
        }
        dirs.push(ComponentDir::try_from(path.as_path())?);
    }

    Ok(dirs)
}

/// Creates a list of direct key certification directories from a directory.
///
/// # Errors
///
/// Returns an error if
///
/// - `path` does not have a subdirectory named [`CERTIFICATION_DIR`], or reading entries from that
///   subdirectory fails,
/// - the [`CERTIFICATION_DIR`] subdirectory contains paths that are not directories,
/// - or creating a [`DirectKeyCertificationDir`] from one of the paths in the [`CERTIFICATION_DIR`]
///   subdirectory fails.
fn direct_key_certification_dirs_from_dir(
    path: impl AsRef<Path>,
) -> Result<Vec<DirectKeyCertificationDir>, crate::Error> {
    let dir = path.as_ref().join(CERTIFICATION_DIR);
    let paths = read_dir_as_sorted_paths(&dir)?;
    let mut dirs = Vec::new();

    for path in paths {
        let metadata = path_metadata(&path)?;

        if !metadata.is_dir() {
            return Err(
                crate::import::Error::DestructuredImport(Error::DirMustContainDirs {
                    dir: dir.to_path_buf(),
                    path: path.clone(),
                })
                .into(),
            );
        }

        dirs.push(DirectKeyCertificationDir::try_from(path.as_path())?);
    }

    Ok(dirs)
}

/// The representation of a direct key certification directory.
///
/// A direct key certification directory consists of one directory with
/// one or more direct key signature packets.
///
/// ```text
/// E242ED3BFFCCDF271B7FBAF34ED72D089537B42F
/// ├── 2024-04-01_15-17-48.asc
/// └── 2024-11-05_18-52-46.asc
/// ```
pub(crate) struct DirectKeyCertificationDir(Vec<PathBuf>);

impl DirectKeyCertificationDir {
    /// Returns the list of tracked regular file paths.
    pub fn paths(&self) -> Vec<PathBuf> {
        self.0.to_vec()
    }
}

impl TryFrom<&Path> for DirectKeyCertificationDir {
    type Error = crate::Error;

    /// Creates a new [`DirectKeyCertificationDir`] from a [`Path`] reference.
    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        let mut paths = Vec::new();
        files_in_dir(value, &mut paths)?;

        Ok(Self(paths))
    }
}

/// The representation of a component directory.
///
/// A component directory must contain exactly one top-level OpenPGP packet file and may contain
/// zero or more OpenPGP packet files in the [`CERTIFICATION_DIR`] and/or [`REVOCATION_DIR`]
/// subdirectories.
/// It may be used to describe OpenPGP User ID directories
///
/// ```text
/// John_Doe__jdoe@example.org_d2ad250f
/// ├── John_Doe__jdoe@example.org_d2ad250f.asc
/// └── certification
///     ├── B787A81C32997FD39A5F4C0188363902D3586E7B.asc
///     ├── 2072A695613E5103D9AC03C2885C5E2656CB5FF0.asc
///     ├── 68D61AF364B99AD0226A9C8859F18BF95A99BCE9.asc
///     ├── 033DB9A2637803F63BDA651106B2C4BEF184C21D.asc
///     ├── 868672B9CDB0BF449BF3782CFDA1DBE372838AA3.asc
///     ├── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
///     ├── 98EECC29ABC53C31B0DA5C85CB26CE720C7FF763.asc
///     └── 52428846EFFD79371A81D6C82D00FBFED9C654F3.asc
/// ```
///
/// or subkey directories in an Arch Linux keyring structure.
///
/// ```text
/// E242ED3BFFCCDF271B7FBAF34ED72D089537B42F
/// ├── E242ED3BFFCCDF271B7FBAF34ED72D089537B42F.asc
/// └── certification
///     └── F1D2D2F924E986AC86FDF7B36C94BCDF32BEEC15.asc
/// ```
pub(crate) struct ComponentDir {
    top_level: PathBuf,
    certifications: Vec<PathBuf>,
    revocations: Vec<PathBuf>,
}

impl ComponentDir {
    /// Returns the list of tracked regular file paths.
    ///
    /// The top-level regular file path is returned before any regular file paths in the
    /// "revocation" or "certification" subdirectories.
    pub fn paths(&self) -> Vec<PathBuf> {
        [
            vec![self.top_level.clone()],
            self.certifications.clone(),
            self.revocations.clone(),
        ]
        .concat()
    }
}

impl TryFrom<&Path> for ComponentDir {
    type Error = crate::Error;

    /// Creates a new [`ComponentDir`] from a [`Path`] reference.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - `value` is not a directory, or entries in it cannot be read,
    /// - an entry in `value` cannot be read,
    /// - metadata for a path in `value` cannot be retrieved,
    /// - there is more than one top-level regular file in `value`,
    /// - the directory [`CERTIFICATION_DIR`] exists in `value` but regular files from it cannot be
    ///   retrieved,
    /// - the directory [`REVOCATION_DIR`] exists in `value` but regular files from it cannot be
    ///   retrieved,
    /// - a directory not named [`CERTIFICATION_DIR`] or [`REVOCATION_DIR`] exists in `value`,
    /// - a path in `value` is not a regular file or a directory,
    /// - or there is not top-level regular file present in `value`.
    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        let paths = read_dir_as_sorted_paths(value)?;
        let mut top_level = None;
        let mut certifications = Vec::new();
        let mut revocations = Vec::new();

        for path in paths {
            let metadata = path_metadata(&path)?;

            if metadata.is_file() {
                if top_level.is_some() {
                    return Err(crate::import::Error::DestructuredImport(
                        Error::MultipleTopLevelPackets {
                            path: value.to_path_buf(),
                        },
                    )
                    .into());
                }

                top_level = Some(path.clone());
            } else if metadata.is_dir() {
                if path.ends_with(CERTIFICATION_DIR) {
                    files_in_dir(path, &mut certifications)?;
                } else if path.ends_with(REVOCATION_DIR) {
                    files_in_dir(path, &mut revocations)?;
                } else {
                    return Err(crate::import::Error::DestructuredImport(
                        Error::InvalidComponentSubDirectory { path: path.clone() },
                    )
                    .into());
                }
            } else {
                return Err(crate::import::Error::DestructuredImport(
                    Error::DirMustContainRegularFilesOrDirs {
                        dir: value.to_path_buf(),
                        path: path.clone(),
                    },
                )
                .into());
            }
        }

        let Some(top_level) = top_level else {
            return Err(
                crate::import::Error::DestructuredImport(Error::NoTopLevelPacket {
                    path: value.to_path_buf(),
                })
                .into(),
            );
        };

        Ok(Self {
            top_level,
            certifications,
            revocations,
        })
    }
}

/// Collects all regular files in a directory in the order of an [OpenPGP Transferable Public
/// Key].
///
/// All top-level regular files in the directory are considered.
/// Regular files located in the following list of directories (if they exist) are considered, in
/// the following order:
///
/// - `revocation`: for Revocation Signature packets
/// - `directkey`: for Direct Key Signature packets
/// - `uid`: for User ID or User Attribute packets
/// - `subkey`: for Subkey packets
///
/// # Errors
///
/// Returns an error if
///
/// - entries in `path` cannot be retrieved,
/// - reading an entry in `path` fails,
/// - or collecting files in any of the subdirectories (`revocation`, `directkey`, `uid` or
///   `subkey`) fails.
///
/// [OpenPGP Transferable Public Key]: https://www.rfc-editor.org/rfc/rfc9580#name-transferable-public-keys
fn collect_files_in_dir(path: impl AsRef<Path>) -> Result<Vec<PathBuf>, crate::Error> {
    let path = path.as_ref();
    debug!("Collecting regular files in {path:?}");

    let paths = read_dir_as_sorted_paths(path)?;
    let mut top_level: Vec<PathBuf> = Vec::new();
    let mut revocation: Vec<PathBuf> = Vec::new();
    let mut directkey: Vec<PathBuf> = Vec::new();
    let mut subkey: Vec<PathBuf> = Vec::new();
    let mut uid: Vec<PathBuf> = Vec::new();

    for file_path in paths {
        let metadata = path_metadata(&file_path)?;

        if !(metadata.is_dir() || metadata.is_file()) {
            return Err(crate::import::Error::DestructuredImport(
                Error::DirMustContainRegularFilesOrDirs {
                    dir: path.to_path_buf(),
                    path: file_path.clone(),
                },
            )
            .into());
        }

        if file_path.is_file() {
            debug!("Found regular file {file_path:?}");
            top_level.push(file_path);
            continue;
        }

        if metadata.is_dir() && top_level.len() > 1 {
            return Err(
                crate::import::Error::DestructuredImport(Error::InvalidFlatStructure {
                    path: path.to_path_buf(),
                })
                .into(),
            );
        }

        if file_path.ends_with(REVOCATION_DIR) {
            trace!("Found a directory for Revocation Signature packets");
            files_in_dir(&file_path, &mut revocation)?;
        } else if file_path.ends_with(DIRECTKEY_DIR) {
            trace!("Found a directory for Direct Key Signature packets");
            for direct_key_certification_dir in direct_key_certification_dirs_from_dir(&file_path)?
            {
                directkey.append(&mut direct_key_certification_dir.paths())
            }
        } else if file_path.ends_with(UID_DIR) {
            trace!("Found a directory for User ID or User Attribute packets");
            for user_id_dir in component_dirs_from_dir(&file_path)? {
                uid.append(&mut user_id_dir.paths())
            }
        } else if file_path.ends_with(SUBKEY_DIR) {
            trace!("Found a directory for Subkey packets");
            for subkey_dir in component_dirs_from_dir(&file_path)? {
                subkey.append(&mut subkey_dir.paths())
            }
        } else {
            return Err(crate::import::Error::DestructuredImport(
                Error::InvalidComponentSubDirectory {
                    path: file_path.clone(),
                },
            )
            .into());
        }
    }

    top_level.sort();
    revocation.sort();
    directkey.sort();

    // If there are no top-level OpenPGP packets, but some in subdirectories, this is not a valid
    // Arch Linux keyring structure.
    if top_level.is_empty()
        && (!revocation.is_empty()
            || !directkey.is_empty()
            || !uid.is_empty()
            || !subkey.is_empty())
    {
        return Err(crate::import::Error::DestructuredImport(
            Error::InvalidArchLinuxKeyringStructure {
                path: path.to_path_buf(),
            },
        )
        .into());
    }

    // If there is more than one top-level OpenPGP packet and any OpenPGP packets in one of the Arch
    // Linux keyring specific subdirectories, this is not a valid flat structure.
    if top_level.len() > 1
        && (!revocation.is_empty()
            || !directkey.is_empty()
            || !uid.is_empty()
            || !subkey.is_empty())
    {
        return Err(
            crate::import::Error::DestructuredImport(Error::InvalidFlatStructure {
                path: path.to_path_buf(),
            })
            .into(),
        );
    }

    Ok([top_level, revocation, directkey, uid, subkey].concat())
}

/// Recognizes a _single_ [OpenPGP packet] in a reader.
///
/// # Note
///
/// The `path` parameter is meant to reflect the file path from which the reader is created.
/// It is only used for better error reporting.
///
/// # Errors
///
/// Returns an error if
///
/// - an OpenPGP packet cannot be parsed from `reader`,
/// - there is no OpenPGP packet in `reader`,
/// - there is at least one additional OpenPGP packet in `reader`,
/// - or there is unparsable data after an initial `OpenPGP` packet.
///
/// [OpenPGP packet]: https://openpgp.dev/book/zoom/certificates.html
fn parse_packet_from_reader<T: Read>(
    reader: BufReader<T>,
    path: &Path,
) -> Result<Packet, crate::Error> {
    let mut packet_parser = PacketParser::new(reader);

    let packet = match packet_parser.next() {
        Some(Ok(packet)) => packet,
        Some(Err(source)) => {
            return Err(crate::Error::OpenPgpPath {
                path: path.to_path_buf(),
                context: "parsing an OpenPGP packet from a buffer",
                source,
            });
        }
        None => {
            return Err(
                crate::import::Error::DestructuredImport(Error::NoPacketInFile {
                    path: path.to_path_buf(),
                })
                .into(),
            );
        }
    };

    match packet_parser.next() {
        Some(Ok(packet)) => {
            return Err(
                crate::import::Error::DestructuredImport(Error::ExcessPacket {
                    path: path.to_path_buf(),
                    tag: packet.tag(),
                })
                .into(),
            );
        }
        Some(Err(source)) => {
            return Err(crate::Error::OpenPgpPath {
                path: path.to_path_buf(),
                context: "parsing an excess OpenPGP packet from a buffer",
                source,
            });
        }
        None => {}
    }

    Ok(packet)
}

/// Reads a _single_ OpenPGP packet from a file.
///
/// The file contents may be binary or ASCII-armored.
///
/// # Errors
///
/// Returns an error if
///
/// - the file at `path` cannot be opened for reading,
/// - the first byte of the file at `path` cannot be read,
/// - the file at `path` is empty,
/// - or if not exactly one OpenPGP packet is found in the file at `path`.
fn read_packet_from_file(path: impl AsRef<Path>) -> Result<Packet, crate::Error> {
    let path = path.as_ref();
    debug!("Reading a single OpenPGP packet from file {path:?}");

    let file = File::open(path).map_err(|source| crate::Error::IoPath {
        path: path.to_path_buf(),
        context: "reading the file",
        source,
    })?;
    let mut reader = BufReader::new(file);

    // Check whether the file contains OpenPGP binary or ASCII-armored data.
    //
    // NOTE: In OpenPGP binary data, the highest bit of the first byte is a one.
    //       The first bit in ASCII is **always** `0`, which makes this a solid heuristic.
    let is_binary = {
        // Read (at least) the first byte into a buffer, without consuming it from the reader.
        let buffer = reader.fill_buf().map_err(|source| crate::Error::IoPath {
            path: path.to_path_buf(),
            context: "filling the buffer",
            source,
        })?;
        if buffer.is_empty() {
            return Err(
                crate::import::Error::DestructuredImport(Error::FileIsEmpty {
                    path: path.to_path_buf(),
                })
                .into(),
            );
        }

        // If the highest bit of the first byte is set, we assume this is OpenPGP binary data.
        buffer[0] & 0x80 != 0
    };

    if is_binary {
        parse_packet_from_reader(reader, path)
    } else {
        parse_packet_from_reader(BufReader::new(Dearmor::new(reader)), path)
    }
}

/// Creates a _single_ [`SignedPublicKey`] from regular files in a directory.
///
/// First collects the paths of all regular files in `path`.
/// Then parses each regular file as a _single_ [OpenPGP packet].
/// Finally, reads a _single_ [OpenPGP certificate] from the packets.
///
/// # Errors
///
/// Returns an error if
///
/// - recursively collecting regular files from `path` fails,
/// - parsing a _single_ OpenPGP packet from each collected regular file fails,
/// - no [`SignedPublicKey`] can be created from the OpenPGP packets,
/// - creating a [`SignedPublicKey`] from the OpenPGP packets fails,
/// - an additional, unwanted [`SignedPublicKey`] is created from the OpenPGP packets,
/// - or creating an additional, unwanted [`SignedPublicKey`] from the OpenPGP packets fails.
///
/// [OpenPGP packet]: https://openpgp.dev/book/zoom/certificates.html
/// [OpenPGP certificate]: https://openpgp.dev/book/certificates.html
fn signed_public_key_from_dir(path: impl AsRef<Path>) -> Result<SignedPublicKey, crate::Error> {
    let path = path.as_ref();
    debug!("Reading a single OpenPGP certificate from OpenPGP packets in directory {path:?}");

    let paths = collect_files_in_dir(path)?;
    let mut packets: Vec<_> = Vec::new();

    for file_path in paths.iter() {
        debug!("Reading regular file {file_path:?} as an OpenPGP packet");
        packets.push(Ok(read_packet_from_file(file_path)?));
    }

    let mut cert_iter = SignedPublicKey::from_packets(packets.into_iter().peekable());
    let pubkey = match cert_iter.next() {
        Some(Ok(cert)) => cert,
        Some(Err(source)) => {
            return Err(crate::Error::OpenPgpPath {
                path: path.to_path_buf(),
                context: "reading an OpenPGP certificate from OpenPGP packets in a directory",
                source,
            });
        }
        None => {
            return Err(
                crate::import::Error::DestructuredImport(Error::NoOpenPgpCertInDir {
                    path: path.to_path_buf(),
                })
                .into(),
            );
        }
    };

    match cert_iter.next() {
        Some(Ok(cert)) => {
            return Err(
                crate::import::Error::DestructuredImport(Error::ExcessCertificateInDir {
                    path: path.to_path_buf(),
                    fingerprint: cert.fingerprint(),
                })
                .into(),
            );
        }
        Some(Err(source)) => {
            return Err(crate::Error::OpenPgpPath {
                path: path.to_path_buf(),
                context: "finding additional, unwanted OpenPGP packets in a directory",
                source,
            });
        }
        None => {}
    }

    Ok(pubkey)
}

/// Creates an [`OpenPgpImport`] from a directory containing OpenPGP packet files.
///
/// Recursively collects all regular files in the directory, concatenates them and
/// attempts to create a single [`SignedPublicKey`] from the accumulated data.
/// Supports both binary and ASCII-armored data.
///
/// The collected regular files must be sorted in the order of an [OpenPGP Transferable Public
/// Key].
/// Both **flat** and **Arch Linux keyring** structures are supported (see the
/// [`import::destructured`][crate::import::destructured] module documentation for details).
///
/// # Errors
///
/// Returns an error if
///
/// - `path` is not a directory,
/// - or a [`SignedPublicKey`] cannot be created from the accumulated data.
///
/// [OpenPGP Transferable Public Key]: https://www.rfc-editor.org/rfc/rfc9580#name-transferable-public-keys
pub fn load_from_dir(path: impl AsRef<Path>) -> Result<OpenPgpImport, crate::Error> {
    let path = path.as_ref();
    debug!("Reading an OpenPGP certificate from directory: {path:?}");

    if !path.is_dir() {
        return Err(
            crate::import::Error::DestructuredImport(Error::PathIsNotADir {
                path: path.to_path_buf(),
            })
            .into(),
        );
    }

    Ok(OpenPgpImport(signed_public_key_from_dir(path)?))
}

#[cfg(test)]
mod tests {

    use std::fs::{create_dir_all, remove_file};

    use log::{info, warn};
    use pgp::{
        armor::{BlockType, write as armor_write},
        bytes::Buf,
        composed::{
            EncryptionCaps,
            KeyType,
            SecretKeyParamsBuilder,
            SignedSecretKey,
            SubkeyParamsBuilder,
        },
        crypto::{hash::HashAlgorithm, sym::SymmetricKeyAlgorithm},
        packet::{
            Features,
            KeyFlags,
            PacketParser,
            PacketTrait,
            RevocationCode,
            SignatureConfig,
            SignatureType,
            Subpacket,
            SubpacketData,
        },
        ser::Serialize,
        types::{Fingerprint, Password, SigningKey, Tag, Timestamp},
    };
    use rand::thread_rng;
    use rstest::{fixture, rstest};
    use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
    use tempfile::tempdir;
    use testresult::TestResult;
    use voa_core::VerifierWriter;

    use super::*;

    /// The configuration for splitting of input files.
    enum SplitConfig {
        /// Place all split OpenPGP packet files in the top-level directory.
        Flat,
        /// Place split OpenPGP packet files in subdirectory structures.
        ArchLinuxKeyring,
    }

    /// The configuration for tests.
    struct TestConfig {
        /// Whether to ASCII-armor the input file(s).
        pub armor: bool,
        /// Whether to split the input file into files per packet.
        pub split: SplitConfig,
    }

    /// Init logger
    fn init_logger() {
        if TermLogger::init(
            LevelFilter::Trace,
            Config::default(),
            TerminalMode::Stderr,
            ColorChoice::Auto,
        )
        .is_err()
        {
            debug!("Not initializing another logger, as one is initialized already.");
        }
    }

    /// The current component when iterating over packets of an OpenPGP certificate.
    ///
    /// Tracks either subkey fingerprints or User IDs.
    enum CurrentComponent {
        /// No current component yet.
        None,
        /// A primary key.
        Primary,
        /// A subkey.
        Subkey(Fingerprint),
        /// A User ID.
        UserId(String),
    }

    /// Splits an OpenPGP certificate into a list of packets in a single directory.
    ///
    /// The packets are written to separate files in `path`.
    /// If `armor` is `true`, each packet is written ASCII-armored using [`BlockType::PublicKey`].
    fn split_openpgp_cert_flat(
        openpgp_cert: &SignedPublicKey,
        path: impl AsRef<Path>,
        armor: bool,
    ) -> TestResult {
        let path = path.as_ref();
        info!("Splitting OpenPGP certificate into packets in a top-level directory.");
        for (i, packet_result) in PacketParser::new(openpgp_cert.to_bytes()?.reader()).enumerate() {
            let packet = packet_result?;
            let packet_file = path.join(format!("{:06}-{:?}", i, packet.tag()));
            info!("Creating packet file {packet_file:?}");
            let mut file = File::create(packet_file)?;
            if armor {
                armor_write(&packet, BlockType::PublicKey, &mut file, None, true)?;
            } else {
                packet.to_writer(&mut file)?;
            }
        }
        Ok(())
    }

    /// Writes a single OpenPGP `packet` file to a directory `path`.
    ///
    /// Creates the parent directory.
    /// If `armor` is `true`, each packet is written ASCII-armored using [`BlockType::PublicKey`].
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - the parent directory cannot be created,
    /// - creating a file at `path` fails,
    /// - or writing the `packet` data to `path` fails.
    fn write_openpgp_packet_file_to_dir(
        path: impl AsRef<Path>,
        packet: Packet,
        armor: bool,
    ) -> TestResult {
        let path = path.as_ref();

        if let Some(parent) = path.parent() {
            create_dir_all(parent)?;
        }

        info!("Creating packet file {path:?}");
        let mut file = File::create(path)?;
        if armor {
            armor_write(&packet, BlockType::PublicKey, &mut file, None, true)?;
        } else {
            packet.to_writer(&mut file)?;
        }

        Ok(())
    }

    /// Splits the OpenPGP certificate `openpgp_cert` into packets in a directory structure.
    ///
    /// The packets are written to separate files in a directory structure in `path`.
    /// If `armor` is `true`, each packet is written ASCII-armored using [`BlockType::PublicKey`].
    ///
    /// # Note
    ///
    /// This implementation is mostly compatible with the Arch Linux keyring structure.
    /// However, it does not (yet) adjust the names of User ID directories and packet files
    /// accordingly.
    /// For test purposes this is sufficient though.
    fn split_openpgp_cert_arch_linux(
        openpgp_cert: &SignedPublicKey,
        path: impl AsRef<Path>,
        armor: bool,
    ) -> TestResult {
        let path = path.as_ref();

        info!("Splitting OpenPGP certificate into packets in subdirectories.");
        let pubkey_fingerprint: Fingerprint = openpgp_cert.fingerprint();
        let mut current_component = CurrentComponent::None;

        for packet_result in PacketParser::new(openpgp_cert.to_bytes()?.reader()) {
            let packet = packet_result?;

            match &packet {
                Packet::PublicKey(_) => {
                    let file_path = path.join(format!("{pubkey_fingerprint}.asc"));
                    current_component = CurrentComponent::Primary;

                    write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                }
                Packet::PublicSubkey(subkey) => {
                    let fingerprint = subkey.fingerprint();
                    let file_path = path
                        .join(SUBKEY_DIR)
                        .join(fingerprint.to_string())
                        .join(format!("{fingerprint}.asc"));
                    current_component = CurrentComponent::Subkey(fingerprint.clone());

                    write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                }
                Packet::UserId(user_id) => {
                    let user_id_string = user_id
                        .as_str()
                        .ok_or("The User ID contains invalid UTF-8")?
                        .to_string();
                    let file_path = path
                        .join(UID_DIR)
                        .join(&user_id_string)
                        .join(format!("{user_id_string}.asc"));
                    current_component = CurrentComponent::UserId(user_id_string.clone());

                    write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                }
                Packet::Signature(signature) => {
                    info!(
                        "Found OpenPGP signature packet of type: {:?}",
                        signature.typ()
                    );
                    // NOTE: There may be zero or more "Issuer Fingerprint" subpackets.
                    //
                    // - Older signatures may have no "Isser Fingerprint" subpacket at all, but
                    //   instead rely on "Issuer" subpackets. This test setup currently only deals
                    //   with the modern "Issuer Fingerprint" scenario.
                    // - If one or more "Issuer Fingerprint" subpackets exist, we only use the first
                    //   for this test setup.
                    let issuer_fingerprints: Vec<Fingerprint> = signature
                        .issuer_fingerprint()
                        .into_iter()
                        .cloned()
                        .collect();
                    let issuer_fingerprint = issuer_fingerprints
                        .as_slice()
                        .first()
                        .map(|f| f.to_string())
                        .unwrap_or("empty".to_string());

                    match signature.typ() {
                        Some(SignatureType::SubkeyBinding) => 'inner: {
                            let CurrentComponent::Subkey(subkey_fingerprint) = &current_component
                            else {
                                warn!(
                                    "Skipping SubkeyBinding, because there is no subkey fingerprint"
                                );
                                break 'inner;
                            };

                            let file_path = path
                                .join(SUBKEY_DIR)
                                .join(subkey_fingerprint.to_string())
                                .join(CERTIFICATION_DIR)
                                .join(format!("{issuer_fingerprint}.asc"));

                            write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                        }
                        Some(SignatureType::CertPositive)
                        | Some(SignatureType::CertGeneric) => 'inner: {
                            let CurrentComponent::UserId(user_id) = &current_component else {
                                warn!("Skipping CertPositive, because there is no User ID");
                                break 'inner;
                            };
                            let file_path = path
                                .join(UID_DIR)
                                .join(user_id)
                                .join(CERTIFICATION_DIR)
                                .join(format!("{issuer_fingerprint}.asc"));

                            write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                        }
                        Some(SignatureType::CertRevocation) => 'inner: {
                            let CurrentComponent::UserId(user_id) = &current_component else {
                                warn!("Skipping CertRevocation, because there is no User ID");
                                break 'inner;
                            };
                            let file_path = path
                                .join(UID_DIR)
                                .join(user_id)
                                .join(REVOCATION_DIR)
                                .join(format!("{issuer_fingerprint}.asc"));

                            write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                        }
                        Some(SignatureType::Key) => {
                            let created_at_date = signature
                                .created()
                                .map(|d| d.as_secs().to_string())
                                .unwrap_or("empty".to_string());
                            let file_path = path
                                .join(DIRECTKEY_DIR)
                                .join(CERTIFICATION_DIR)
                                .join(issuer_fingerprint)
                                .join(format!("{created_at_date}.asc"));

                            write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                        }
                        Some(SignatureType::KeyRevocation) => {
                            let file_path = path
                                .join(REVOCATION_DIR)
                                .join(format!("{}.asc", issuer_fingerprint));

                            write_openpgp_packet_file_to_dir(file_path, packet.clone(), armor)?;
                        }
                        _ => {
                            warn!("Not using unknown signature packet {signature:?}");
                        }
                    }
                }
                _ => {
                    warn!("Not using unknown packet {packet:?}");
                }
            }
        }
        Ok(())
    }

    /// Splits an OpenPGP certificate into separate OpenPGP packets.
    ///
    /// The packets are written to separate files in `path`.
    /// If `armor` is `true`, each packet is written ASCII-armored using [`BlockType::PublicKey`].
    fn split_openpgp_cert(
        openpgp_cert: &SignedPublicKey,
        path: impl AsRef<Path>,
        armor: bool,
        config: SplitConfig,
    ) -> TestResult {
        let path = path.as_ref();

        match config {
            SplitConfig::Flat => split_openpgp_cert_flat(openpgp_cert, path, armor)?,
            SplitConfig::ArchLinuxKeyring => {
                split_openpgp_cert_arch_linux(openpgp_cert, path, armor)?
            }
        }

        Ok(())
    }

    /// Creates a baseline OpenPGP private key for further specialization.
    fn openpgp_private_key() -> TestResult<SignedSecretKey> {
        let mut signkey = SubkeyParamsBuilder::default();
        signkey
            .key_type(KeyType::Ed25519Legacy)
            .can_sign(true)
            .can_encrypt(EncryptionCaps::None)
            .can_authenticate(false);
        let mut key_params = SecretKeyParamsBuilder::default();
        key_params
            .key_type(KeyType::Ed25519Legacy)
            .can_certify(true)
            .can_sign(false)
            .can_encrypt(EncryptionCaps::None)
            .primary_user_id("John Doe <jdoe@example.org>".to_string())
            .subkeys(vec![signkey.build()?]);

        let secret_key_params = key_params.build()?;
        Ok(secret_key_params.generate(thread_rng())?)
    }

    /// Creates an OpenPGP certificate ([`SignedPublicKey`]).
    #[fixture]
    fn openpgp_cert() -> TestResult<SignedPublicKey> {
        let private = openpgp_private_key()?;

        Ok(SignedPublicKey::from(private))
    }

    /// Creates an OpenPGP certificate ([`SignedPublicKey`]) with a direct key signature.
    fn openpgp_cert_dks() -> TestResult<SignedPublicKey> {
        let mut private = openpgp_private_key()?;

        //  additionally produce a direct key signature with the certificate metadata
        let mut config =
            SignatureConfig::v4(SignatureType::Key, private.algorithm(), private.hash_alg());

        let mut flags = KeyFlags::default();
        flags.set_certify(true);

        let mut features = Features::default();
        features.set_seipd_v1(true);

        config.hashed_subpackets = vec![
            Subpacket::regular(SubpacketData::SignatureCreationTime(Timestamp::now()))?,
            Subpacket::regular(SubpacketData::IssuerFingerprint(private.fingerprint()))?,
            Subpacket::regular(SubpacketData::KeyFlags(flags))?,
            Subpacket::regular(SubpacketData::Features(features))?,
            Subpacket::regular(SubpacketData::PreferredSymmetricAlgorithms(
                vec![
                    SymmetricKeyAlgorithm::AES256,
                    SymmetricKeyAlgorithm::AES192,
                    SymmetricKeyAlgorithm::AES128,
                ]
                .into(),
            ))?,
            Subpacket::regular(SubpacketData::PreferredHashAlgorithms(
                vec![
                    HashAlgorithm::Sha256,
                    HashAlgorithm::Sha384,
                    HashAlgorithm::Sha512,
                ]
                .into(),
            ))?,
        ];

        let dks = config.sign_key(
            &private.primary_key,
            &Password::empty(),
            private.primary_key.public_key(),
        )?;
        private.details.direct_signatures.push(dks);

        Ok(SignedPublicKey::from(private))
    }

    /// Creates an OpenPGP certificate ([`SignedPublicKey`]) with a revocation signature.
    ///
    /// The resulting OpenPGP certificate is considered "revoked".
    fn openpgp_cert_revoked() -> TestResult<SignedPublicKey> {
        let mut private = openpgp_private_key()?;

        // produce a revocation signature and add it to the top level key
        let mut config = SignatureConfig::v4(
            SignatureType::KeyRevocation,
            private.algorithm(),
            private.hash_alg(),
        );

        let mut flags = KeyFlags::default();
        flags.set_certify(true);

        let mut features = Features::default();
        features.set_seipd_v1(true);

        config.hashed_subpackets = vec![
            Subpacket::regular(SubpacketData::SignatureCreationTime(Timestamp::now()))?,
            Subpacket::regular(SubpacketData::IssuerFingerprint(private.fingerprint()))?,
            Subpacket::regular(SubpacketData::RevocationReason(
                RevocationCode::KeyRetired,
                "Key has been replaced by 0x1234".into(),
            ))?,
        ];

        let revocation = config.sign_key(
            &private.primary_key,
            &Password::empty(),
            private.primary_key.public_key(),
        )?;
        private.details.revocation_signatures.push(revocation);

        let pubkey = SignedPublicKey::from(private);
        Ok(pubkey)
    }

    /// Creates an OpenPGP certificate ([`SignedPublicKey`]) with a User ID revocation signature.
    ///
    /// The User ID of the resulting OpenPGP certificate is considered "revoked".
    fn openpgp_cert_revoked_primary_user() -> TestResult<SignedPublicKey> {
        let mut private = openpgp_private_key()?;

        // produce a revocation signature and add it to the top level key
        let mut config = SignatureConfig::v4(
            SignatureType::CertRevocation,
            private.algorithm(),
            private.hash_alg(),
        );

        let mut flags = KeyFlags::default();
        flags.set_certify(true);

        let mut features = Features::default();
        features.set_seipd_v1(true);

        config.hashed_subpackets = vec![
            Subpacket::regular(SubpacketData::SignatureCreationTime(Timestamp::now()))?,
            Subpacket::regular(SubpacketData::IssuerFingerprint(private.fingerprint()))?,
            Subpacket::regular(SubpacketData::RevocationReason(
                RevocationCode::CertUserIdInvalid,
                "This User ID is now invalid".into(),
            ))?,
        ];

        let revocation = config.sign_certification(
            &private.primary_key,
            private.primary_key.public_key(),
            &Password::empty(),
            Tag::UserId,
            &private.details.users[0].id,
        )?;
        private.details.users[0].signatures.push(revocation);

        let pubkey = SignedPublicKey::from(private);
        Ok(pubkey)
    }

    /// Ensures that [`OpenPgpImport`] can be created from OpenPGP certificate.
    ///
    /// Further ensures that the export of the verifier with [`OpenPgpImport::write_to_hierarchy`]
    /// works.
    ///
    /// Provided OpenPGP packet files are created based on a [`TestConfig`].
    #[rstest]
    #[case::split_top_level_binary(TestConfig{armor: false, split: SplitConfig::Flat}, openpgp_cert())]
    #[case::split_top_level_armor(TestConfig{armor: true, split: SplitConfig::Flat}, openpgp_cert())]
    #[case::split_dirs_binary(TestConfig{armor: false, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert())]
    #[case::split_dirs_armor(TestConfig{armor: true, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert())]
    #[case::split_top_level_binary_dks(TestConfig{armor: false, split: SplitConfig::Flat}, openpgp_cert_dks())]
    #[case::split_top_level_armor_dks(TestConfig{armor: true, split: SplitConfig::Flat}, openpgp_cert_dks())]
    #[case::split_dirs_binary_dks(TestConfig{armor: false, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_dks())]
    #[case::split_dirs_armor_dks(TestConfig{armor: true, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_dks())]
    #[case::split_top_level_binary_revoked(TestConfig{armor: false, split: SplitConfig::Flat}, openpgp_cert_revoked())]
    #[case::split_top_level_armor_revoked(TestConfig{armor: true, split: SplitConfig::Flat}, openpgp_cert_revoked())]
    #[case::split_dirs_binary_revoked(TestConfig{armor: false, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_revoked())]
    #[case::split_dirs_armor_revoked(TestConfig{armor: true, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_revoked())]
    #[case::split_top_level_binary_revoked_primary_user(TestConfig{armor: false, split: SplitConfig::Flat}, openpgp_cert_revoked_primary_user())]
    #[case::split_top_level_armor_revoked_primary_user(TestConfig{armor: true, split: SplitConfig::Flat}, openpgp_cert_revoked_primary_user())]
    #[case::split_dirs_binary_revoked_primary_user(TestConfig{armor: false, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_revoked_primary_user())]
    #[case::split_dirs_armor_revoked_primary_user(TestConfig{armor: true, split: SplitConfig::ArchLinuxKeyring}, openpgp_cert_revoked_primary_user())]
    fn write_to_hierarchy_succeeds(
        #[case] config: TestConfig,
        #[case] cert: TestResult<SignedPublicKey>,
    ) -> TestResult {
        init_logger();

        let input_pubkey = cert?;

        let temp_dir = tempdir()?;
        let path = temp_dir.path().to_path_buf();
        split_openpgp_cert(&input_pubkey, &path, config.armor, config.split)?;

        let import = load_from_dir(path.as_path())?;

        let temp_dir = tempdir()?;
        let output_dir = temp_dir.path();
        import.write_to_hierarchy(
            output_dir,
            "os".parse()?,
            "purpose".parse()?,
            Some("context".parse()?),
        )?;

        let output_file = output_dir
            .join("os")
            .join("purpose")
            .join("context")
            .join(import.technology().to_string())
            .join(import.file_name());

        let (output_pubkey, _) = SignedPublicKey::from_armor_file(&output_file)?;

        assert_eq!(input_pubkey, output_pubkey);

        Ok(())
    }

    /// Ensures that [`openpgp_import_from_destructured_dir`] fails on invalid flat structures.
    #[rstest]
    fn openpgp_import_from_destructured_dir_fails_on_invalid_flat_structure(
        openpgp_cert: TestResult<SignedPublicKey>,
    ) -> TestResult {
        init_logger();

        let input_pubkey = openpgp_cert?;

        let temp_dir = tempdir()?;
        let path = temp_dir.path().to_path_buf();

        // Write both flat and Arch Linux keyring structures to the import directory.
        // This renders this structure an invalid flat structure.
        split_openpgp_cert(&input_pubkey, &path, true, SplitConfig::Flat)?;
        split_openpgp_cert(&input_pubkey, &path, true, SplitConfig::ArchLinuxKeyring)?;

        match load_from_dir(path.as_path()) {
            Ok(verifier) => {
                panic!("Should have failed, but succeeded to create a verifier: {verifier:?}");
            }
            Err(error) => match error {
                crate::Error::Import(crate::import::error::Error::DestructuredImport(
                    Error::InvalidFlatStructure { .. },
                )) => {}
                error => {
                    panic!("Did not return the correct error, got: {error}");
                }
            },
        }

        Ok(())
    }

    /// Ensures that [`openpgp_import_from_destructured_dir`] fails on invalid Arch Linux keyring
    /// structures.
    #[rstest]
    fn openpgp_import_from_destructured_dir_fails_on_invalid_arch_linux_keyring_structure(
        openpgp_cert: TestResult<SignedPublicKey>,
    ) -> TestResult {
        init_logger();

        let input_pubkey = openpgp_cert?;

        let temp_dir = tempdir()?;
        let path = temp_dir.path().to_path_buf();
        split_openpgp_cert(&input_pubkey, &path, true, SplitConfig::ArchLinuxKeyring)?;

        // Remove the only top-level file in the import directory.
        // This renders this structure an invalid Arch Linux keyring structure
        for dir_entry in path.read_dir()? {
            let dir_entry = dir_entry?;
            let path = dir_entry.path();
            if path.is_file() {
                remove_file(path)?;
            }
        }

        match load_from_dir(path.as_path()) {
            Ok(verifier) => {
                panic!("Should have failed, but succeeded to create a verifier: {verifier:?}");
            }
            Err(error) => match error {
                crate::Error::Import(crate::import::error::Error::DestructuredImport(
                    Error::InvalidArchLinuxKeyringStructure { .. },
                )) => {}
                error => {
                    panic!("Did not return the correct error, got: {error}");
                }
            },
        }

        Ok(())
    }

    /// Ensures that [`OpenPgpImport::new`] fails if the input path is not a file or a directory.
    #[test]
    #[cfg(target_os = "linux")]
    fn openpgp_import_from_destructured_dir_fails_on_path_not_a_dir() -> TestResult {
        match load_from_dir("/dev/null") {
            Ok(verifier) => {
                panic!("Should have failed, but succeeded to create a verifier: {verifier:?}");
            }
            Err(error) => match error {
                crate::Error::Import(crate::import::error::Error::DestructuredImport(
                    crate::import::destructured::error::Error::PathIsNotADir { .. },
                )) => {}
                error => {
                    panic!("Did not return the correct error, got: {error}");
                }
            },
        }
        Ok(())
    }
}
