/// This file implements a finder algorithm within deserialization of the settings
/// It is much more efficient to do it this way, way less memory allocation and manipulation
/// Only the settings that are needed are kept in memory
use std::{
    collections::HashMap,
    fs,
    io::BufReader,
    path::{Path, PathBuf},
};

use api::{Api, ApiEvent};
use bon::Builder;
use de::{ConfigFinderDeserializer, DConfigFinder, DLinkedCommand, DLinkedRole, DLinkedTask};
use log::debug;
use options::BorrowedOptStack;
use rar_common::{
    Cred,
    database::{
        actor::{DGroupType, DGroups},
        options::{SAuthentication, SBounding, SPrivileged, STimeout, SUMask, WorkdirBehavior},
        score::{CmdMin, CmdOrder, HardenedBool, Score, hardened_bool_from_bool},
    },
    util::{StorageMethod, WORKDIR_BEHAVIOR, all_paths_from_env, read_with_privileges},
};
use serde::de::DeserializeSeed;

use crate::{
    Cli,
    error::{SrError, SrResult},
    finder::{
        de::{cred::CredOwnedData, settings::read_storage},
        options::DWorkdirSet,
    },
};

pub mod api;
mod cmd;
pub mod de;
mod options;

#[derive(Debug, Default, Clone, Builder)]
pub struct BestExecSettings {
    #[builder(default)]
    /// The final matching score. Evaluated over several fields, see `Score`
    pub score: Score,

    #[builder(default)]
    /// The execution path, canonalized and sanitized
    pub final_path: PathBuf,

    #[builder(default)]
    /// The Owned version of credentials needed for switching user and set capabilities
    pub cred: CredOwnedData,

    ///The task name matched in the policy
    pub task: Option<String>,

    #[builder(default)]
    /// The role name matched in the policy
    pub role: String,
    #[builder(default)]
    /// The final set of environment variable to keep/set
    pub env: HashMap<String, String>,
    #[builder(default)]
    /// The PATH variable is managed indepedently given the policy
    pub env_path: Vec<String>,
    /// The working directory to set, if specified in policy
    pub workdir: Option<PathBuf>,
    #[builder(default)]
    /// Whether the Linux Capabilities are [bounded](https://www.man7.org/linux/man-pages/man7/capabilities.7.html)
    pub bounding: SBounding,
    #[builder(default)]
    /// Information about whether the user should re-authenticate
    pub timeout: STimeout,
    #[builder(default)]
    /// Information about whether the user should authenticate or bypass it
    pub auth: SAuthentication,
    #[builder(default)]
    /// Is root id has it's privileges or not? If not, root is going to be a simple user
    pub root: SPrivileged,
    #[builder(default)]
    /// Setting umask
    pub umask: SUMask,
}

/// This functions is the main entrace to lookup at the security policy.
/// It efficiently check the policy based on the user args, skips unnecessary policy info etc.
/// The main focus here is to avoid at maximum any allocation.
/// # Returns
/// When a policy match was found, it returns all needed information, otherwise, it returns an ``SrError``.
pub fn find_best_exec_settings<'de: 'a, 'a, P>(
    cli: &'a Cli,
    cred: &'a Cred,
    rar_cfg_path: P,
    rar_cfg_data_path: P,
    rar_cfg_type: StorageMethod,
    env_vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
    env_path: &[&str],
) -> SrResult<BestExecSettings>
where
    P: AsRef<Path>,
{
    let env_vars: Vec<(String, String)> = env_vars
        .into_iter()
        .map(|(key, value)| (key.into(), value.into()))
        .collect();
    let settings_file = read_storage(rar_cfg_path, rar_cfg_type)?;
    let file_path = settings_file
        .data
        .data
        .settings
        .unwrap_or_default()
        .path
        .unwrap_or_else(|| rar_cfg_data_path.as_ref().to_path_buf());
    let parse_file = |file_path: &Path| -> SrResult<BestExecSettings> {
        let config_finder_deserializer = ConfigFinderDeserializer {
            cli,
            cred,
            env_path,
        };
        let file = read_with_privileges(file_path)?;
        let reader = BufReader::new(file);
        match settings_file.data.data.method {
            StorageMethod::CBOR => {
                let mut io_reader = cbor4ii::core::utils::IoReader::new(reader);
                Ok(BestExecSettings::retrieve_settings(
                    cli,
                    cred,
                    &config_finder_deserializer
                        .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader))
                        .map_err(|e| {
                            debug!("Error deserializing CBOR: {e}");
                            SrError::ConfigurationError
                        })?,
                    env_vars.iter().cloned(),
                    env_path,
                )?)
            }
            StorageMethod::JSON => {
                let io_reader = serde_json::de::IoRead::new(reader);
                Ok(BestExecSettings::retrieve_settings(
                    cli,
                    cred,
                    &config_finder_deserializer
                        .deserialize(&mut serde_json::Deserializer::new(io_reader))
                        .map_err(|e| {
                            debug!("Error deserializing JSON: {e}");
                            SrError::ConfigurationError
                        })?,
                    env_vars.iter().cloned(),
                    env_path,
                )?)
            }
        }
    };

    if file_path.is_dir() {
        let mut entries: Vec<PathBuf> = fs::read_dir(&file_path)
            .map_err(|e| {
                debug!(
                    "Failed to read policy directory {}: {e}",
                    file_path.display()
                );
                SrError::ConfigurationError
            })?
            .flatten()
            .map(|entry| entry.path())
            .filter(|path| path.is_file())
            .collect();
        entries.sort();

        let mut best: Option<BestExecSettings> = None;
        let mut parsed_any = false;

        for entry in entries {
            let result = parse_file(&entry);
            match result {
                Ok(settings) => {
                    parsed_any = true;
                    if best.as_ref().is_none_or(|best_settings| {
                        settings.score.better_fully(&best_settings.score)
                    }) {
                        best = Some(settings);
                    }
                }
                Err(SrError::PermissionDenied) => {
                    parsed_any = true;
                }
                Err(err) => {
                    debug!("Skipping policy file {}: {err}", entry.display());
                }
            }
        }

        return best.ok_or({
            if parsed_any {
                SrError::PermissionDenied
            } else {
                SrError::ConfigurationError
            }
        });
    }

    parse_file(&file_path)
}

impl BestExecSettings {
    fn retrieve_settings<'a>(
        cli: &'a Cli,
        cred: &'a Cred,
        data: &'a DConfigFinder<'a>,
        env_vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
        env_path: &[&str],
    ) -> SrResult<Self> {
        let mut result = Self::default();
        let mut matching = false;
        let mut opt_stack = BorrowedOptStack::new(data.options.clone());
        for role in data.roles() {
            matching |= result.role_settings(cli, cred, &role, &mut opt_stack, env_path)?;
            Api::notify(ApiEvent::BestRoleSettingsFound(
                cli,
                cred,
                &role,
                &mut opt_stack,
                &env_path,
                &mut result,
                &mut matching,
            ))?;
        }
        if !matching {
            return Err(SrError::PermissionDenied);
        }
        result.env = opt_stack
            .calc_temp_env(opt_stack.calc_override_behavior(), cli.opt_filter.as_ref())
            .calc_final_env(
                env_vars,
                opt_stack.calc_path(env_path),
                cred,
                result.cred.setuid.as_ref(),
                format!(
                    "{}{}",
                    cli.cmd_path.display(),
                    if cli.cmd_args.is_empty() {
                        String::new()
                    } else {
                        format!(" {}", cli.cmd_args.join(" "))
                    }
                ),
            )?;
        result.auth = opt_stack.calc_authentication();
        result.bounding = opt_stack.calc_bounding();
        result.timeout = opt_stack.calc_timeout();
        result.root = opt_stack.calc_privileged();
        result.umask = opt_stack.calc_umask();
        result.workdir = opt_stack.calc_workdir();
        Ok(result)
    }

    pub fn role_settings<'c, 'a>(
        &mut self,
        cli: &'c Cli,
        cred: &'c Cred,
        data: &DLinkedRole<'c, 'a>,
        opt_stack: &mut BorrowedOptStack<'a>,
        env_path: &[&str],
    ) -> SrResult<bool> {
        debug!("role_settings: {:?}", data.role().role);
        if !self.actors_settings(data)? {
            return Ok(false);
        }
        let mut res = false;
        for task in data.tasks() {
            res |= self.task_settings(cli, cred, &task, opt_stack, env_path)?;
        }
        Ok(res)
    }

    pub fn actors_settings(&mut self, data: &DLinkedRole<'_, '_>) -> SrResult<bool> {
        let mut res = !data.role().user_min.is_no_match();
        Api::notify(ApiEvent::ActorMatching(data, self, &mut res))?;
        Ok(res)
    }

    #[allow(clippy::too_many_lines)]
    pub fn task_settings<'t, 'a>(
        &mut self,
        cli: &'t Cli,
        cred: &'t Cred,
        data: &DLinkedTask<'t, '_, 'a>,
        opt_stack: &mut BorrowedOptStack<'a>,
        env_path: &[&str],
    ) -> SrResult<bool> {
        debug!("task_settings: {:?}", data.id);
        let temp_opt_stack = BorrowedOptStack::from_task(data);
        let mut found = false;
        let mut f_env_path = None;
        // We must do this check for each task as long they could have different options
        // These checks are a small optimization to avoid useless command checks
        if cli
            .opt_filter
            .as_ref()
            .is_some_and(|f| f.env_behavior.is_some() && !temp_opt_stack.calc_override_behavior())
        {
            debug!(
                "task_settings: deny task due to inherited from role or config env_override requirement"
            );
            return Ok(false);
        }
        let result = temp_opt_stack.get_workdir_temp();
        if cli
            .opt_filter
            .as_ref()
            .and_then(|f| f.workdir.as_ref())
            .is_some_and(|w| Self::allowed_workdir(&result, w.as_str()).is_false())
        {
            debug!(
                "task_settings: deny task due to the user wanted to execute to a specific folder, which do not match to settings"
            );
            return Ok(false);
        }
        if Self::allowed_workdir(&result, cred.curdir.as_os_str().to_string_lossy().as_ref())
            .is_false()
        {
            debug!(
                "task_settings: deny task due to the current directory do not match to allowed settings"
            );
            return Ok(false);
        }
        if cli.info && temp_opt_stack.calc_info().is_hide() {
            debug!("task_settings: deny task due to inherited from role or config info hide");
            return Ok(false);
        }
        if let Some(commands) = data.commands() {
            let t_env_path = opt_stack.calc_path(env_path);
            for command in commands.del() {
                if self.command_settings(
                    &t_env_path
                        .iter()
                        .map(std::string::String::as_str)
                        .collect::<Vec<_>>(),
                    cli,
                    &command,
                )? {
                    return Ok(false);
                }
            }
            if commands.default_behavior.is_some_and(|b| b.is_all()) {
                debug!("default behavior is all");
                let t_env_path = opt_stack.calc_path(env_path);
                found = true;
                debug!("{}", &cli.cmd_path.display());
                if let Ok(path) = cli.cmd_path.canonicalize() {
                    self.final_path = path;
                } else {
                    self.final_path.clone_from(
                        all_paths_from_env(
                            &t_env_path
                                .iter()
                                .map(std::string::String::as_str)
                                .collect::<Vec<_>>(),
                            &cli.cmd_path,
                        )
                        .first()
                        .ok_or(SrError::ExecutionFailed)?,
                    );
                }
                self.score.cmd_min = CmdMin::builder()
                    .matching()
                    .order(CmdOrder::FullWildcardPath | CmdOrder::RegexArgs)
                    .build();
            } else {
                for command in commands.add() {
                    if self.command_settings(
                        &t_env_path
                            .iter()
                            .map(std::string::String::as_str)
                            .collect::<Vec<_>>(),
                        cli,
                        &command,
                    )? {
                        found = true;
                        break;
                    }
                }
            }
            f_env_path = Some(t_env_path);
        } else if let Some(final_path) = &data.final_path {
            debug!("final_path already found: {}", final_path.display());
            found = self.update_command_score(final_path.clone(), data.score.cmd_min);
        }
        let mut score = data.score(self.score.cmd_min, temp_opt_stack.calc_security_min());
        Api::notify(ApiEvent::BestTaskSettingsFound(
            cli, data, opt_stack, self, &mut score,
        ))?;
        if found && score.better_fully(&self.score) {
            debug!("found better task settings");
            self.role = data.role().role().role.to_string();
            self.task = Some(data.id.to_string());
            self.env_path = f_env_path
                .unwrap_or_else(|| opt_stack.calc_path(env_path))
                .iter()
                .map(std::string::ToString::to_string)
                .collect();
            self.score = score;
            self.cred.setuid = data.cred.setuid.clone().and_then(|u| u.fetch_user());
            self.cred.setgroups = data.cred.setgroups.clone().map(|g| match g {
                DGroups::Single(g) => vec![g.fetch_group()].into_iter().flatten().collect(),
                DGroups::Multiple(g) => g.iter().filter_map(DGroupType::fetch_group).collect(),
            });
            self.cred.caps = data.cred.caps;
            opt_stack.set_role(data);
            opt_stack.set_task(data);
            debug!("resulting settings: {self:?}");
        }

        Ok(found)
    }

    pub fn command_settings<'d>(
        &mut self,
        env_path: &[&str],
        cli: &'d Cli,
        data: &DLinkedCommand<'d, '_, '_, '_, '_>,
    ) -> SrResult<bool> {
        debug!("env_path: {env_path:?}");
        Ok(match &**data {
            de::DCommand::Simple(role_cmd) => {
                let mut final_path = None;
                let cmd_min = cmd::evaluate_command_match(
                    env_path,
                    &cli.cmd_path,
                    &cli.cmd_args,
                    role_cmd,
                    self.score.cmd_min,
                    &mut final_path,
                );
                final_path.is_some_and(|final_path| self.update_command_score(final_path, cmd_min))
            }
            de::DCommand::Complex(value) => {
                let mut cmd_min = CmdMin::empty();
                let mut final_path = None;
                Api::notify(ApiEvent::ProcessComplexCommand(
                    value,
                    env_path,
                    &cli.cmd_path,
                    &cli.cmd_args,
                    &mut cmd_min,
                    &mut final_path,
                ))?;
                final_path.is_some_and(|final_path| self.update_command_score(final_path, cmd_min))
            }
        })
    }

    /// This function takes a workdir path, and returns whether it is allowed based on the workdir settings.
    fn allowed_workdir(result: &DWorkdirSet<'_>, workdir: &str) -> HardenedBool {
        // Apply logic based on final behavior
        match result.default_behavior {
            WorkdirBehavior::Allowlist => {
                // Block all except what is in "add" minus "sub"
                let is_in_add = result.add.iter().any(|path| path.as_ref() == workdir);
                let is_in_sub = result.sub.iter().any(|path| path.as_ref() == workdir);
                hardened_bool_from_bool(is_in_add && !is_in_sub)
            }
            WorkdirBehavior::Blacklist => {
                // Allow all except what is in "sub"
                hardened_bool_from_bool(!result.sub.iter().any(|path| path.as_ref() == workdir))
            }
            WorkdirBehavior::Inherit => {
                // Should not happen after union, but default to WORKDIR_BEHAVIOR
                hardened_bool_from_bool(WORKDIR_BEHAVIOR.is_blacklist())
            }
        }
    }

    fn update_command_score(&mut self, final_path: PathBuf, res: CmdMin) -> bool {
        debug!(
            "update_command_score: current score {:?}, new score {:?}",
            self.score.cmd_min, res
        );
        if res.better(self.score.cmd_min) {
            debug!("better");
            self.score.cmd_min = res;
            self.final_path = final_path;
            true
        } else {
            debug!("not better");
            false
        }
    }
}

#[cfg(test)]
mod tests {
    use super::de::{DCommand, DCommandList, DRoleFinder, DTaskFinder, IdTask};
    use super::*;
    use capctl::CapSet;
    use rar_common::database::FilterMatcher;
    use rar_common::database::options::{EnvBehavior, Level, SInfo};
    use rar_common::database::score::{ActorMatchMin, CmdMin, Score};
    use rar_common::database::structs::SetBehavior;
    use serde_json::Value;
    use std::path::PathBuf;

    use crate::Cli;
    use crate::finder::de::cred::CredData;
    use crate::finder::options::{DEnvOptions, Opt};
    use rar_common::Cred;
    use test_log::test;

    // Helper: Dummy implementations for required traits/structs
    fn dummy_cli() -> Cli {
        Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .build()
    }

    fn dummy_cred() -> Cred {
        Cred::builder()
            .curdir()
            .unwrap()
            .groups()
            .unwrap()
            .user()
            .unwrap()
            .build()
    }

    fn dummy_dconfigfinder<'a>() -> DConfigFinder<'a> {
        DConfigFinder::builder()
            .roles(vec![
                DRoleFinder::builder()
                    .user_min(ActorMatchMin::UserMatch)
                    .role("test")
                    .tasks(vec![
                        DTaskFinder::builder()
                            .id(IdTask::Number(0))
                            .cred(CredData::builder().caps(!CapSet::empty()).build())
                            .commands(
                                DCommandList::builder(SetBehavior::None)
                                    .add(vec![DCommand::simple("/usr/bin/ls -l")])
                                    .build(),
                            )
                            .options(Opt::builder(Level::Task).execinfo(SInfo::Hide).build())
                            .build(),
                        DTaskFinder::builder()
                            .id(IdTask::Number(1))
                            .cred(CredData::builder().caps(CapSet::empty()).build())
                            .commands(
                                DCommandList::builder(SetBehavior::None)
                                    .add(vec![
                                        DCommand::simple("/usr/bin/ls ^.*$"),
                                        DCommand::complex(Value::Object(
                                            std::iter::once((
                                                "key".to_string(),
                                                Value::String("value".into()),
                                            ))
                                            .collect::<serde_json::Map<String, Value>>(),
                                        )),
                                    ])
                                    .build(),
                            )
                            .options(
                                Opt::builder(Level::Task)
                                    .execinfo(SInfo::Show)
                                    .env(
                                        DEnvOptions::builder(EnvBehavior::Delete)
                                            .override_behavior(true)
                                            .build(),
                                    )
                                    .build(),
                            )
                            .build(),
                    ])
                    .build(),
                DRoleFinder::builder()
                    .user_min(ActorMatchMin::UserMatch)
                    .role("test2")
                    .tasks(vec![
                        DTaskFinder::builder()
                            .id(IdTask::Number(0))
                            .cred(CredData::builder().caps(!CapSet::empty()).build())
                            .commands(
                                DCommandList::builder(SetBehavior::None)
                                    .add(vec![DCommand::simple("/usr/bin/ls -l")])
                                    .build(),
                            )
                            .options(
                                Opt::builder(Level::Task)
                                    .execinfo(SInfo::Show)
                                    .env(
                                        DEnvOptions::builder(EnvBehavior::Delete)
                                            .override_behavior(true)
                                            .build(),
                                    )
                                    .build(),
                            )
                            .build(),
                        DTaskFinder::builder()
                            .id(IdTask::Number(1))
                            .cred(CredData::builder().caps(CapSet::empty()).build())
                            .commands(
                                DCommandList::builder(SetBehavior::None)
                                    .add(vec![DCommand::simple("/usr/bin/ls ^.*$")])
                                    .build(),
                            )
                            .options(Opt::builder(Level::Task).execinfo(SInfo::Hide).build())
                            .build(),
                    ])
                    .build(),
            ])
            .build()
    }

    #[test]
    fn test_retrieve_settings_no_matching_role() {
        let cli = Cli::builder().cmd_path("/usr/bin/cat".to_string()).build();
        let cred = dummy_cred();
        let data = dummy_dconfigfinder();
        let env_vars = vec![("KEY", "VALUE")];
        let env_path = &["/bin"];
        let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path);
        assert!(result.is_err());
    }

    #[test]
    fn test_retrieve_settings_with_matching_role() {
        let cli = dummy_cli();
        let cred = dummy_cred();
        let data = dummy_dconfigfinder();
        let env_vars = vec![("KEY", "VALUE")];
        let env_path = &["/UNWANTED"];
        let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path);
        assert!(result.is_ok());
        let settings = result.unwrap();
        assert_eq!(settings.final_path, PathBuf::from("/usr/bin/ls"));
        assert_eq!(settings.role, "test");
        assert_eq!(settings.task, Some("0".to_string()));
        assert!(settings.cred.setuid.is_none());
        assert!(settings.cred.setgroups.is_none());
        assert!(settings.cred.caps.is_some());
        assert!(!settings.env.is_empty());
        assert!(!settings.env_path.is_empty());
        assert!(settings.env_path.iter().all(|p| p != "/UNWANTED"));
    }

    #[test]
    fn test_role_settings_calls_actors_and_tasks() {
        let mut best = BestExecSettings::default();
        let cli = dummy_cli();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let data = binding.roles().next().unwrap();
        let mut opt_stack = BorrowedOptStack::new(None);
        let env_path = &["/bin"];
        let result = best.role_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok());
    }

    #[test]
    fn test_actors_settings_returns_bool() {
        let mut best = BestExecSettings::default();
        let binding = dummy_dconfigfinder();
        let data = binding.roles().next().unwrap();
        let result = best.actors_settings(&data);
        assert!(result.is_ok());
        assert!(result.is_ok());
    }

    #[test]
    fn test_task_settings_sets_fields_on_found() {
        let mut best = BestExecSettings::default();
        let cli = dummy_cli();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let data = binding.tasks().next().unwrap();
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/bin"];
        let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok_and(|r| r));
        assert!(*best.final_path == *"/usr/bin/ls");
        assert!(best.role == "test");
        assert!(best.task == Some("0".to_string()));
        assert!(best.cred.caps.is_some());
        assert!(best.score.cmd_min == CmdMin::MATCH);
    }

    #[cfg(feature = "pcre2")]
    #[test]
    fn test_command_settings_simple_and_complex() {
        let mut best = BestExecSettings::default();
        let cli = dummy_cli();
        let env_path = &["/usr/bin"];
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let binding = binding.tasks().nth(1).unwrap();
        let binding = binding.commands().unwrap();
        let data = binding.add().next().unwrap();
        let result = best.command_settings(env_path, &cli, &data);
        assert!(result.as_ref().is_ok_and(|b| *b));
        let data = binding.add().nth(1).unwrap();
        let result = best.command_settings(env_path, &cli, &data);
        assert!(
            result.as_ref().is_ok_and(|b| !*b),
            "Failed to process complex command : {}",
            result.unwrap_err()
        );
    }

    #[test]
    fn test_update_command_score_better() {
        let mut settings = BestExecSettings {
            score: Score {
                cmd_min: CmdMin::builder()
                    .matching()
                    .order(CmdOrder::RegexArgs)
                    .build(),
                ..Default::default()
            },
            final_path: PathBuf::from("/old/path"),
            ..Default::default()
        };
        let new_cmd_min = CmdMin::MATCH;
        let new_path = PathBuf::from("/new/path");
        let updated = settings.update_command_score(new_path.clone(), new_cmd_min);
        assert!(updated);
        assert_eq!(settings.score.cmd_min, new_cmd_min);
        assert_eq!(settings.final_path, new_path);
    }

    #[test]
    fn test_update_command_score_not_better() {
        let mut settings = BestExecSettings {
            score: Score {
                cmd_min: CmdMin::MATCH,
                ..Default::default()
            },
            final_path: PathBuf::from("/old/path"),
            ..Default::default()
        };
        let worse_cmd_min = CmdMin::builder()
            .matching()
            .order(CmdOrder::RegexArgs)
            .build();
        let new_path = PathBuf::from("/new/path");
        let updated = settings.update_command_score(new_path, worse_cmd_min);
        assert!(!updated);
        assert_eq!(settings.final_path, PathBuf::from("/old/path"));
    }

    #[test]
    fn test_info_denied_due_to_inherited_hide_one() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .info()
            .build();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let binding = binding.tasks().next().unwrap();
        // This task has info hide set in options, it should be denied
        let data = binding;
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(!result.unwrap());
    }
    #[test]
    fn test_info_denied_due_to_inherited_hide_two() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .info()
            .build();
        let cred = dummy_cred();
        // Now test with the second task which does not have info hide
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let binding = binding.tasks().nth(1).unwrap();
        // This task has info hide set in options, it should be denied
        let data = binding;
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
    }
    #[test]
    fn test_info_denied_due_to_inherited_hide_three() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .info()
            .build();
        let cred = dummy_cred();
        // Now try best.role_settings to ensure full flow works
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let data = binding.tasks().nth(1).unwrap();
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(*best.final_path == *"/usr/bin/ls");
        assert!(best.role == "test", "role was {}", best.role);
        assert!(best.task == Some("1".to_string()));
    }
    #[test]
    fn test_info_denied_due_to_inherited_hide_four() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .info()
            .build();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().nth(1).unwrap();
        let data = binding.tasks().next().unwrap();
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(*best.final_path == *"/usr/bin/ls");
        assert!(best.role == "test2", "role was {}", best.role);
        assert!(best.task == Some("0".to_string()));
    }

    #[test]
    fn test_info_denied_due_to_inherited_env_override_one() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .opt_filter(
                FilterMatcher::builder()
                    .env_behavior(EnvBehavior::Keep)
                    .build(),
            )
            .build();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let binding = binding.tasks().next().unwrap();
        // This task has info hide set in options, it should be denied
        let data = binding;
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(!result.unwrap());
    }
    #[test]
    fn test_info_denied_due_to_inherited_env_override_two() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .opt_filter(
                FilterMatcher::builder()
                    .env_behavior(EnvBehavior::Keep)
                    .build(),
            )
            .build();
        let cred = dummy_cred();
        // Now test with the second task which does not have info hide
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let binding = binding.tasks().nth(1).unwrap();
        // This task has info hide set in options, it should be denied
        let data = binding;
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
    }
    #[test]
    fn test_info_denied_due_to_inherited_env_override_three() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .opt_filter(
                FilterMatcher::builder()
                    .env_behavior(EnvBehavior::Keep)
                    .build(),
            )
            .build();
        let cred = dummy_cred();
        // Now try best.role_settings to ensure full flow works
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().next().unwrap();
        let data = binding.tasks().nth(1).unwrap();
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(*best.final_path == *"/usr/bin/ls");
        assert!(best.role == "test", "role was {}", best.role);
        assert!(best.task == Some("1".to_string()));
    }
    #[test]
    fn test_info_denied_due_to_inherited_env_override_four() {
        let mut best = BestExecSettings::default();
        let cli = Cli::builder()
            .cmd_path("/usr/bin/ls".to_string())
            .cmd_args(vec!["-l".to_string()])
            .opt_filter(
                FilterMatcher::builder()
                    .env_behavior(EnvBehavior::Keep)
                    .build(),
            )
            .build();
        let cred = dummy_cred();
        let binding = dummy_dconfigfinder();
        let binding = binding.roles().nth(1).unwrap();
        let data = binding.tasks().next().unwrap();
        let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone());
        let env_path = &["/usr/bin"];
        let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(*best.final_path == *"/usr/bin/ls");
        assert!(best.role == "test2", "role was {}", best.role);
        assert!(best.task == Some("0".to_string()));
    }
}
