//! Traits used for writing of verifiers in VOA hierarchies.

use std::{
    fs::{File, create_dir_all},
    io::{BufWriter, Write},
    path::{Path, PathBuf},
};

use log::trace;

use crate::{
    Error,
    identifiers::{Context, Os, Purpose, Technology},
};

/// A trait that provides functionality for writing a [VOA] verifier to a VOA hierarchy.
///
/// # Note
///
/// By default, [`VerifierWriter`] allows to write to arbitrary locations.
/// While [`VerifierWriter::write_to_hierarchy`] enables the user to write a verifier to the correct
/// location in a hierarchy, this trait does not concern itself with where that hierarchy is
/// located, nor does it consider existing [symlinking] or [masking] rules.
///
/// This trait is meant to provide basic functionality for writing verifiers to a VOA hierarchy.
/// Users of this trait must care for applying its functionality on the correct location (see [load
/// paths]) and handle [symlinking] and [masking] separately to comply with the strict rules that
/// [`Voa::lookup`][crate::Voa] enforces.
///
/// # Examples
///
/// ```
/// use std::{fs::read_to_string, path::PathBuf};
///
/// use voa_core::{
///     Error,
///     VerifierWriter,
///     identifiers::{CustomTechnology, Technology},
/// };
///
/// const VERIFIER_DATA: &str = "test";
///
/// // A test struct implementing `VerifierWrite`
/// struct TestWriter;
///
/// impl VerifierWriter for TestWriter {
///     fn to_bytes(&self) -> Result<Vec<u8>, Error> {
///         Ok(VERIFIER_DATA.as_bytes().to_vec())
///     }
///
///     fn technology(&self) -> Technology {
///         Technology::Custom(CustomTechnology::new("technology".parse().unwrap()))
///     }
///
///     fn file_name(&self) -> PathBuf {
///         PathBuf::from("dummy.test")
///     }
/// }
///
/// # fn main() -> testresult::TestResult {
/// let test_dir = tempfile::tempdir()?;
/// let path = test_dir.path();
/// let test_writer = TestWriter;
///
/// // Write the verifier to a temporary directory.
/// test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None)?;
///
/// // Ensure that the contents match.
/// let verifier_file = path
///     .join("os")
///     .join("purpose")
///     .join("default")
///     .join("technology")
///     .join(test_writer.file_name());
/// let verifier_contents = read_to_string(verifier_file)?;
/// assert_eq!(verifier_contents, VERIFIER_DATA);
/// # Ok(())
/// # }
/// ```
///
/// [VOA]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/
/// [load path]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#load-paths
/// [symlinking]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking
/// [masking]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#masking
pub trait VerifierWriter {
    /// Returns the verifier as bytes.
    ///
    /// # Errors
    ///
    /// Returns an error, if the verifier can not be returned.
    fn to_bytes(&self) -> Result<Vec<u8>, Error>;

    /// Returns the [`Technology`] used by the verifier.
    fn technology(&self) -> Technology;

    /// Returns the file name of the verifier.
    ///
    /// File names depend on the [`Technology`] used by the verifier.
    fn file_name(&self) -> PathBuf;

    /// Writes the verifier to a VOA hierarchy.
    ///
    /// The VOA hierarchy directory is provided using `path`.
    /// Using `os`, `purpose` and `context` and the specific technology (see
    /// [`VerifierWriter::technology`]), the correct directory for the verifier is created in
    /// `path`. Afterwards, the verifier data is written to the specific file name (see
    /// [`VerifierWriter::file_name`]).
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - the parent directory for the verifier cannot be created,
    /// - the verifier file cannot be created,
    /// - or the verifier data cannot be written to the file.
    fn write_to_hierarchy(
        &self,
        path: impl AsRef<Path>,
        os: Os,
        purpose: Purpose,
        context: Option<Context>,
    ) -> Result<(), Error> {
        let context = context.unwrap_or_default();
        let path = path.as_ref();
        let target_dir = path
            .join(os.to_string())
            .join(purpose.to_string())
            .join(context.to_string())
            .join(self.technology().to_string());

        trace!("Create parent directory for verifier: {target_dir:?}");
        create_dir_all(&target_dir).map_err(|source| Error::IoPath {
            path: target_dir.clone(),
            context: "creating the directory",
            source,
        })?;

        let file_path = target_dir.join(self.file_name());
        trace!("Write verifier data to file: {file_path:?}");
        // Fail if the target exists, but is not a file.
        if file_path.exists() && !file_path.is_file() {
            return Err(Error::ExpectedFile {
                path: file_path.to_path_buf(),
            });
        }
        let mut writer =
            BufWriter::new(File::create(&file_path).map_err(|source| Error::IoPath {
                path: file_path.clone(),
                context: "creating file for writing",
                source,
            })?);
        writer
            .write_all(&self.to_bytes()?)
            .map_err(|source| Error::IoPath {
                path: file_path,
                context: "writing the verifier contents",
                source,
            })?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::{fs::read_to_string, os::unix::fs::symlink};

    use log::debug;
    use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
    use tempfile::tempdir;
    use testresult::TestResult;

    use super::*;
    use crate::identifiers::CustomTechnology;

    const VERIFIER_DATA: &str = "test";

    struct TestWriter;

    impl VerifierWriter for TestWriter {
        fn to_bytes(&self) -> Result<Vec<u8>, Error> {
            Ok(VERIFIER_DATA.as_bytes().to_vec())
        }

        fn technology(&self) -> Technology {
            Technology::Custom(CustomTechnology::new("technology".parse().unwrap()))
        }

        fn file_name(&self) -> PathBuf {
            PathBuf::from("dummy.test")
        }
    }

    /// 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.");
        }
    }

    /// Ensures that a verifier implementing [`VerifierWrite`] successfully writes data to a file.
    #[test]
    fn write_to_hierarchy_succeeds() -> TestResult {
        init_logger();

        let test_dir = tempdir()?;
        let path = test_dir.path();
        let test_writer = TestWriter;

        test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None)?;

        let target_dir = path
            .join("os")
            .join("purpose")
            .join("default")
            .join("technology");
        assert!(target_dir.is_dir());

        let verifier_file = target_dir.join(test_writer.file_name());
        assert!(verifier_file.is_file());
        let verifier_contents = read_to_string(verifier_file)?;
        assert_eq!(verifier_contents, VERIFIER_DATA);

        Ok(())
    }

    /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to write to a file
    /// that is occupied by a directory.
    #[test]
    fn write_to_hierarchy_fails_on_target_is_dir() -> TestResult {
        init_logger();

        let test_dir = tempdir()?;
        let path = test_dir.path();
        let test_writer = TestWriter;

        // Create a directory in place of the target file.
        let target_file = path
            .join("os")
            .join("purpose")
            .join("default")
            .join("technology")
            .join(test_writer.file_name());
        create_dir_all(&target_file)?;
        assert!(target_file.is_dir());

        match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
            Ok(()) => {
                panic!(
                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
                );
            }
            Err(error) => match error {
                Error::ExpectedFile { .. } => {}
                error => {
                    panic!("Expected Error::ExpectedFile, but got:\n{error}");
                }
            },
        }

        Ok(())
    }

    /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to write to a file
    /// that is occupied by a symlink.
    #[test]
    fn write_to_hierarchy_fails_on_target_is_symlink() -> TestResult {
        init_logger();

        let test_dir = tempdir()?;
        let path = test_dir.path();
        let test_writer = TestWriter;

        // Create the parent directory.
        let parent_dir = path
            .join("os")
            .join("purpose")
            .join("default")
            .join("technology");
        create_dir_all(&parent_dir)?;
        let target_file = parent_dir.join(test_writer.file_name());
        // Create a symlink to /dev/null in place of the target file.
        symlink("/dev/null", &target_file)?;
        assert!(target_file.is_symlink());

        match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
            Ok(()) => {
                panic!(
                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
                );
            }
            Err(error) => match error {
                Error::ExpectedFile { .. } => {}
                error => {
                    panic!("Expected Error::ExpectedFile, but got:\n{error}");
                }
            },
        }

        Ok(())
    }

    /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to create a
    /// directory structure in which one element is occupied by a file.
    #[test]
    fn write_to_hierarchy_fails_on_dir_structure_has_file() -> TestResult {
        init_logger();

        let test_dir = tempdir()?;
        let path = test_dir.path();
        let test_writer = TestWriter;

        // Create the parent directory.
        let parent_dir = path.join("os").join("purpose").join("default");
        create_dir_all(&parent_dir)?;

        // Create a file in place of the last directory element.
        let parent_file = parent_dir.join("technology");
        let mut file = File::create(&parent_file)?;
        file.write_all(b"Occupied!")?;
        assert!(parent_file.is_file());
        let target_file = parent_file.join(test_writer.file_name());

        match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
            Ok(()) => {
                panic!(
                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
                );
            }
            Err(error) => match error {
                Error::IoPath { .. } => {}
                error => {
                    panic!("Expected Error::IoPath, but got:\n{error}");
                }
            },
        }

        Ok(())
    }

    /// Ensures that a verifier implementing [`VerifierWrite`] fails when trying to create a
    /// directory structure in which one element is occupied by a symlink.
    #[test]
    fn write_to_hierarchy_fails_on_dir_structure_has_symlink() -> TestResult {
        init_logger();

        let test_dir = tempdir()?;
        let path = test_dir.path();
        let test_writer = TestWriter;

        // Create the parent directory.
        let parent_dir = path.join("os").join("purpose").join("default");
        create_dir_all(&parent_dir)?;

        // Create a symlink in place of the last directory element.
        let parent_symlink = parent_dir.join("technology");
        symlink("/dev/null", &parent_symlink)?;
        assert!(parent_symlink.is_symlink());
        let target_file = parent_symlink.join(test_writer.file_name());

        match test_writer.write_to_hierarchy(path, "os".parse()?, "purpose".parse()?, None) {
            Ok(()) => {
                panic!(
                    "Should have failed but succeeded to write a verifier to the VOA hierarchy at {target_file:?}"
                );
            }
            Err(error) => match error {
                Error::IoPath { .. } => {}
                error => {
                    panic!("Expected Error::IoPath, but got:\n{error}");
                }
            },
        }

        Ok(())
    }
}
