// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Find the path to the just-built executable file to test.
//!
//! Provide a helper function that allows a unit or integration test to
//! execute a Rust program that was just built in this crate or
//! in another crate in the same workspace.

use core::iter;

#[cfg(feature = "test-exe-path")]
use std::env;

#[cfg(feature = "test-exe-path")]
use {
    camino::{Utf8Path, Utf8PathBuf},
    eyre::{Result, WrapErr as _, bail, eyre},
    indexmap::IndexMap,
};

/// Get the list of features supported by the module.
#[inline]
pub fn features() -> impl Iterator<Item = (&'static str, &'static str)> {
    let it_base = iter::once(("test-exe", "0.1"));

    #[cfg(not(feature = "test-exe-path"))]
    let it_test_exe_path = it_base;
    #[cfg(feature = "test-exe-path")]
    let it_test_exe_path = it_base.chain(iter::once(("test-exe-path", "0.1")));

    it_test_exe_path
}

/// Find the path to a single executable file in the specified directory.
///
/// # Errors
///
/// Fail if the specified path does not point to an existing regular file.
#[cfg(feature = "test-exe-path")]
fn find_single_exe_path(name: &str, exe_dir: &Utf8Path) -> Result<Utf8PathBuf> {
    let exe_path = exe_dir.join(name);
    if !exe_path.is_file() {
        bail!("Not a regular file: {exe_path}");
    }
    Ok(exe_path)
}

#[cfg(feature = "test-exe-path")]
/// The map of the results of looking up the individual executables.
pub type PathsMap<'names> = IndexMap<&'names str, Result<Utf8PathBuf>>;

#[cfg(feature = "test-exe-path")]
/// The aggregated result of looking up all of the executable paths.
#[derive(Debug)]
pub struct PathsMapResult<'names>(Result<PathsMap<'names>>);

#[cfg(feature = "test-exe-path")]
impl PathsMapResult<'_> {
    /// Try to look up a single executable's path.
    ///
    /// # Errors
    ///
    /// Propagate errors from the stored result.
    #[inline]
    pub fn get_exe_path(&self, name: &str) -> Result<&Utf8Path> {
        match self.0 {
            Ok(ref paths) => match *(paths
                .get(name)
                .ok_or_else(|| eyre!("Internal error: no path result recorded for {name}"))?)
            {
                Ok(ref path) => Ok(path),
                Err(ref err) => {
                    bail!("Could not determine the path to {name}: {err}");
                }
            },
            Err(ref err) => {
                bail!("Could not determine the path to any of the test programs: {err}");
            }
        }
    }
}

/// Determine the paths to the specified just-compiled executable files.
///
/// # Errors
///
/// Fail if the path to the current executable cannot be determined.
///
/// Record a failure if the path to one of the specified executable files cannot be determined.
#[cfg(feature = "test-exe-path")]
#[inline]
#[must_use]
pub fn find_exe_paths<'names>(exe_names: &[&'names str]) -> PathsMapResult<'names> {
    let do_find = || {
        let current = Utf8PathBuf::from_path_buf(
            env::current_exe().context("Could not get the current executable file's path")?,
        )
        .map_err(|path| {
            eyre!(
                "Could not represent {path} as valid UTF-8",
                path = path.display()
            )
        })?;
        let exe_dir = {
            let basedir = current
                .parent()
                .ok_or_else(|| eyre!("Could not get the parent directory of {current}"))?;
            if basedir
                .file_name()
                .ok_or_else(|| eyre!("Could not get the base name of {basedir}"))?
                == "deps"
            {
                basedir
                    .parent()
                    .ok_or_else(|| eyre!("Could not get the parent directory of {basedir}"))?
            } else {
                basedir
            }
        };

        Ok(exe_names
            .iter()
            .map(|path| (*path, find_single_exe_path(path, exe_dir)))
            .collect())
    };

    PathsMapResult(do_find())
}
