Documentation
use std::{collections::HashMap, path::Path, sync::Arc};

use anyhow::{Ok, Result};
use async_trait::async_trait;
use futures::future::try_join_all;
use itertools::Itertools;
use log::debug;
use rustc_hash::FxHashMap;
use url::Url;

use crate::Hydration;

use super::{Provider, Warnings, add_url, deserialize_output, run_cli};

#[derive(Default)]
pub struct Passbolt {
    urls: FxHashMap<Url, String>,
}

impl Passbolt {
    pub fn new() -> Self {
        Default::default()
    }
}

#[async_trait]
impl Provider for Passbolt {
    fn add(&mut self, value: String) -> Result<()> {
        add_url(&mut self.urls, value, "passbolt")
    }

    fn name(&self) -> &'static str {
        "Passbolt"
    }

    fn install_url(&self) -> &'static str {
        "https://github.com/passbolt/go-passbolt-cli"
    }

    fn has_work(&self) -> bool {
        !self.urls.is_empty()
    }

    async fn resolve(
        &self,
        _: &Path,
        extra_env: &HashMap<String, String>,
        _: &Warnings,
    ) -> Result<Hydration> {
        let extra_env = Arc::new(extra_env.clone());
        let name = self.name();
        let install_url = self.install_url();
        let fetches = self
            .urls
            .iter()
            .into_group_map_by(|(url, _)| url.host().expect("Missing host"))
            .into_iter()
            .flat_map(|(host, group)| {
                let extra_env = Arc::clone(&extra_env);
                group
                    .into_iter()
                    .into_group_map_by(|(url, _)| {
                        url.path().split('/').nth(1).expect("Missing resource id")
                    })
                    .into_iter()
                    .map(move |(resource_id, group)| {
                        let host = host.clone();
                        let extra_env = Arc::clone(&extra_env);
                        async move {
                            let cmd = [
                                "passbolt",
                                "get",
                                "resource",
                                &format!("--serverAddress=https://{}", host),
                                &format!("--id={}", resource_id),
                                "--json",
                            ];
                            debug!("Lade run: {}", cmd.join(" "));

                            let child = run_cli(&cmd, &extra_env, name, install_url, None).await?;
                            let loaded: HashMap<String, String> = deserialize_output(&child, name)?;

                            let hydration = group
                                .into_iter()
                                .map(|(url, value)| {
                                    let var = url.path().split('/').nth(2).expect("Missing field");
                                    (
                                        value.clone(),
                                        loaded
                                            .get(var)
                                            .unwrap_or_else(|| {
                                                panic!(
                                                    "Variable not found in Passbolt: {}",
                                                    resource_id
                                                )
                                            })
                                            .clone(),
                                    )
                                })
                                .collect::<Hydration>();

                            debug!("hydration: {:?}", hydration);
                            Ok(hydration)
                        }
                    })
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>();

        Ok(try_join_all(fetches).await?.into_iter().flatten().collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::providers::fake_cli;
    use std::collections::HashMap;
    use std::path::Path;
    use tempfile::tempdir;

    #[test]
    fn test_add_valid_passbolt_scheme() {
        let mut p = Passbolt::new();
        assert!(
            p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
                .is_ok()
        );
    }

    #[test]
    fn test_add_rejects_wrong_scheme() {
        let mut p = Passbolt::new();
        assert!(p.add("vault://host/mount/key/field".to_string()).is_err());
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_fake_cli() {
        let fake_bin = tempdir().unwrap();
        fake_cli(
            &fake_bin,
            "passbolt",
            r#"echo '{"password":"passbolt_value","username":"user"}'"#,
        );

        let mut p = Passbolt::new();
        p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
            .unwrap();
        let extra = HashMap::from([(
            "PATH".to_string(),
            fake_bin.path().to_string_lossy().into_owned(),
        )]);
        let result = p
            .resolve(Path::new("."), &extra, &Warnings::default())
            .await
            .unwrap();
        assert_eq!(
            result
                .get("passbolt://passbolt.example.com/resource-uuid/password")
                .unwrap(),
            "passbolt_value"
        );
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_malformed_json_error() {
        let fake_bin = tempdir().unwrap();
        fake_cli(&fake_bin, "passbolt", "echo 'not valid json'");

        let mut p = Passbolt::new();
        p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
            .unwrap();
        let extra = HashMap::from([(
            "PATH".to_string(),
            fake_bin.path().to_string_lossy().into_owned(),
        )]);
        let result = p
            .resolve(Path::new("."), &extra, &Warnings::default())
            .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Passbolt error"));
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_cli_not_found() {
        let empty_bin = tempdir().unwrap();
        let mut p = Passbolt::new();
        p.add("passbolt://passbolt.example.com/resource-uuid/password".to_string())
            .unwrap();
        let extra = HashMap::from([(
            "PATH".to_string(),
            empty_bin.path().to_string_lossy().into_owned(),
        )]);
        let result = p
            .resolve(Path::new("."), &extra, &Warnings::default())
            .await;
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Passbolt CLI not found")
        );
    }
}