use ahash::AHashMap;
use fs_err as fs;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::Read;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use base64::engine::{Engine, general_purpose::STANDARD as standard_b64};
use sha2::{Sha256, Sha384, Sha512, digest};

use config::Config;
use tera::{Error, Function, Kwargs, State, TeraResult};
use utils::site::resolve_internal_link;

use crate::helpers::search_for_file;

fn compute_hash<D>(data: &[u8], as_base64: bool) -> String
where
    D: digest::Digest,
    digest::Output<D>: AsRef<[u8]>,
{
    let mut hasher = D::new();
    hasher.update(data);
    let result = hasher.finalize();
    let bytes = result.as_ref();
    if as_base64 {
        standard_b64.encode(bytes)
    } else {
        let mut hex = String::with_capacity(bytes.len() * 2);
        for b in bytes {
            write!(hex, "{:02x}", b).unwrap();
        }
        hex
    }
}

#[derive(Debug, Default)]
pub struct GetUrl {
    base_path: PathBuf,
    config: Config,
    permalinks: HashMap<String, String>,
    output_path: PathBuf,
    colocated_assets: AHashMap<String, (String, String)>,
    result_cache: Arc<Mutex<HashMap<String, String>>>,
}

impl GetUrl {
    pub fn new(
        base_path: PathBuf,
        config: Config,
        permalinks: HashMap<String, String>,
        output_path: PathBuf,
        colocated_assets: AHashMap<String, (String, String)>,
    ) -> Self {
        Self {
            base_path,
            config,
            permalinks,
            output_path,
            colocated_assets,
            result_cache: Arc::new(Mutex::new(HashMap::new())),
        }
    }
}

fn make_path_with_lang(path: String, lang: &str, config: &Config) -> TeraResult<String> {
    if lang == config.default_language {
        return Ok(path);
    }

    if !config.other_languages().contains_key(lang) {
        return Err(Error::message(format!(
            "`{}` is not an authorized language (check config.languages).",
            lang
        )));
    }

    let mut split_path: Vec<String> = path.split('.').map(String::from).collect();
    let ilast = split_path.len() - 1;
    split_path[ilast] = format!("{}.{}", lang, split_path[ilast]);
    Ok(split_path.join("."))
}

impl Function<TeraResult<String>> for GetUrl {
    fn call(&self, kwargs: Kwargs, state: &State) -> TeraResult<String> {
        let path: String = kwargs.must_get("path")?;
        let cachebust: bool = kwargs.get("cachebust")?.unwrap_or(false);
        let trailing_slash: bool = kwargs.get("trailing_slash")?.unwrap_or(false);
        let explicit_lang: Option<String> = kwargs.get("lang")?;

        // if it starts with @/, resolve it as an internal link
        if let Some(stripped) = path.strip_prefix("@/") {
            let lang: String = explicit_lang
                .or(state.get("lang")?)
                .unwrap_or_else(|| self.config.default_language.clone());

            // Check if it's a colocated asset first
            if let Some((owner_md, rel)) = self.colocated_assets.get(stripped) {
                let owner_with_lang =
                    make_path_with_lang(format!("@/{}", owner_md), &lang, &self.config)?;
                return match resolve_internal_link(&owner_with_lang, &self.permalinks) {
                    Ok(resolved) => Ok(format!("{}{}", resolved.permalink, rel)),
                    Err(_) => Err(Error::message(format!(
                        "`get_url`: could not resolve URL for asset `{}` not found.",
                        path
                    ))),
                };
            }

            let path_with_lang = make_path_with_lang(path, &lang, &self.config)?;

            match resolve_internal_link(&path_with_lang, &self.permalinks) {
                Ok(resolved) => Ok(resolved.permalink),
                Err(_) => Err(Error::message(format!(
                    "`get_url`: could not resolve URL for link `{}` not found.",
                    path_with_lang
                ))),
            }
        } else {
            // anything else
            let mut segments = vec![];

            if let Some(ref lang) = explicit_lang
                && *lang != self.config.default_language
            {
                let p = path.strip_prefix('/').unwrap_or(&path);
                if p != lang.as_str() && !p.starts_with(&format!("{lang}/")) {
                    segments.push(lang.clone());
                }
            }
            if segments.is_empty() {
                segments.push(path.clone());
            } else {
                // We've pushed a {lang}/ so remove the / first
                segments.push(path.trim_start_matches('/').to_string());
            }

            let path_with_lang = segments.join("/");

            let mut permalink = self.config.make_permalink(&path_with_lang);
            if !trailing_slash && permalink.ends_with('/') {
                permalink.pop(); // Removes the slash
            }

            if cachebust {
                match search_for_file(
                    &self.base_path,
                    &path_with_lang,
                    &self.config.theme,
                    &self.output_path,
                )
                .map_err(|e| Error::message(format!("`get_url`: {}", e)))?
                .and_then(|(file_path, unified_path)| {
                    let mut cache = self.result_cache.lock().expect("result cache lock");
                    if let Some(hash) = cache.get(&unified_path) {
                        return Some(hash.clone());
                    }
                    let mut f = fs::File::open(file_path).ok()?;
                    let mut contents = Vec::new();
                    f.read_to_end(&mut contents).ok()?;
                    let hash = compute_hash::<Sha256>(&contents, false);
                    cache.insert(unified_path, hash.clone());
                    Some(hash)
                }) {
                    Some(hash) => {
                        let short_hash = &hash[..20]; // 2^-80 chance of false positive
                        permalink = format!("{permalink}?h={short_hash}");
                    }
                    None => {
                        return Err(Error::message(format!(
                            "`get_url`: Could not find or open file {}",
                            path_with_lang
                        )));
                    }
                };
            }

            Ok(permalink)
        }
    }

    fn is_safe(&self) -> bool {
        true
    }
}

#[derive(Debug, Default)]
pub struct GetHash {
    base_path: PathBuf,
    theme: Option<String>,
    output_path: PathBuf,
}

impl GetHash {
    pub fn new(base_path: PathBuf, theme: Option<String>, output_path: PathBuf) -> Self {
        Self { base_path, theme, output_path }
    }
}

impl Function<TeraResult<String>> for GetHash {
    fn call(&self, kwargs: Kwargs, _: &State) -> TeraResult<String> {
        let path: Option<String> = kwargs.get("path")?;
        let literal: Option<String> = kwargs.get("literal")?;

        let contents = match (path, literal) {
            (Some(_), Some(_)) => {
                return Err(Error::message(
                    "`get_hash`: must have only one of `path` or `literal` argument",
                ));
            }
            (None, None) => {
                return Err(Error::message(
                    "`get_hash`: must have at least one of `path` or `literal` argument",
                ));
            }
            (Some(path_v), None) => {
                let file_path =
                    match search_for_file(&self.base_path, &path_v, &self.theme, &self.output_path)
                        .map_err(|e| Error::message(format!("`get_hash`: {}", e)))?
                    {
                        Some((f, _)) => f,
                        None => {
                            return Err(Error::message(format!(
                                "`get_hash`: Cannot find file: {}",
                                path_v
                            )));
                        }
                    };

                let mut f = fs::File::open(&file_path).map_err(|e| {
                    Error::message(format!("File {} could not be open: {}", path_v, e))
                })?;

                let mut contents = Vec::new();
                f.read_to_end(&mut contents).map_err(|e| {
                    Error::message(format!("File {} could not be read: {}", path_v, e))
                })?;

                contents
            }
            (None, Some(literal_v)) => literal_v.into_bytes(),
        };

        let sha_type: u16 = kwargs.get("sha_type")?.unwrap_or(384);
        let base64: bool = kwargs.get("base64")?.unwrap_or(true);

        let hash = match sha_type {
            256 => compute_hash::<Sha256>(&contents, base64),
            384 => compute_hash::<Sha384>(&contents, base64),
            512 => compute_hash::<Sha512>(&contents, base64),
            _ => return Err(Error::message("`get_hash`: Invalid sha value")),
        };

        Ok(hash)
    }
}

#[cfg(test)]
mod tests {
    use super::{GetHash, GetUrl};

    use ahash::AHashMap;
    use fs_err as fs;
    use std::collections::HashMap;
    use std::path::PathBuf;
    use tempfile::{TempDir, tempdir};
    use tera::{Context, Function, Kwargs, State};

    use config::Config;
    use utils::fs::create_file;

    fn create_temp_dir() -> TempDir {
        let dir = tempdir().unwrap();
        create_file(&dir.path().join("app.css"), "// Hello world!").expect("Failed to create file");
        dir
    }

    const CONFIG_DATA: &str = r#"
base_url = "https://remplace-par-ton-url.fr"
default_language = "fr"

[translations]
title = "Un titre"

[languages.en]
[languages.en.translations]
title = "A title"
"#;

    #[test]
    fn can_add_cachebust_to_url() {
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            Config::default(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("cachebust", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/app.css?h=572e691dc68c3fcd653a"
        );

        // And binary files as well
        fs::copy("gutenberg.jpg", dir.path().join("gutenberg.jpg")).unwrap();
        let kwargs = Kwargs::from([
            ("path", tera::Value::from("gutenberg.jpg")),
            ("cachebust", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/gutenberg.jpg?h=93fff9d0ecde9b119c0c"
        );
    }

    #[test]
    fn can_add_trailing_slashes() {
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            Config::default(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("trailing_slash", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/app.css/"
        );
    }

    #[test]
    fn can_add_slashes_and_cachebust() {
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            Config::default(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("trailing_slash", tera::Value::from(true)),
            ("cachebust", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/app.css/?h=572e691dc68c3fcd653a"
        );
    }

    #[test]
    fn can_link_to_some_static_file() {
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            Config::default(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([("path", tera::Value::from("app.css"))]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/app.css"
        );

        let kwargs = Kwargs::from([("path", tera::Value::from("/app.css"))]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/app.css"
        );
    }

    #[test]
    fn can_link_to_file_in_output_path() {
        let dir = create_temp_dir();
        let public = dir.path().join("public");
        fs::create_dir(&public).expect("Failed to create output directory");
        create_file(&public.join("style.css"), "// Hello world")
            .expect("Failed to create file in output directory");

        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            Config::default(),
            HashMap::new(),
            public,
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([("path", tera::Value::from("style.css"))]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "http://a-website.com/style.css"
        );
    }

    #[test]
    fn error_when_language_not_available() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("@/a_section/a_page.md")),
            ("lang", tera::Value::from("it")),
        ]);
        let ctx = Context::new();
        let err = get_url.call(kwargs, &State::new(&ctx)).unwrap_err();
        assert!(err.to_string().contains("is not an authorized language"));
    }

    #[test]
    fn can_get_url_with_default_language() {
        let mut permalinks = HashMap::new();
        permalinks.insert(
            "a_section/a_page.md".to_string(),
            "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(),
        );
        permalinks.insert(
            "a_section/a_page.en.md".to_string(),
            "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
        );
        let config = Config::parse(CONFIG_DATA).unwrap();
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config.clone(),
            permalinks.clone(),
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("@/a_section/a_page.md")),
            ("lang", tera::Value::from("fr")),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "https://remplace-par-ton-url.fr/a_section/a_page/"
        );
    }

    #[test]
    fn can_get_url_with_other_language() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let mut permalinks = HashMap::new();
        permalinks.insert(
            "a_section/a_page.md".to_string(),
            "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(),
        );
        permalinks.insert(
            "a_section/a_page.en.md".to_string(),
            "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
        );
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            permalinks,
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("@/a_section/a_page.md")),
            ("lang", tera::Value::from("en")),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "https://remplace-par-ton-url.fr/en/a_section/a_page/"
        );
    }

    // Colocated assets resolve the owning page/section for the requested language and append the
    // asset's relative path. fr/en use different slugs to prove the lang prefix isn't blindly added.
    #[test]
    fn can_get_colocated_asset_url() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let mut permalinks = HashMap::new();
        permalinks.insert(
            "a_section/an_article/index.md".to_string(),
            "https://remplace-par-ton-url.fr/a_section/mon_article/".to_string(),
        );
        permalinks.insert(
            "a_section/an_article/index.en.md".to_string(),
            "https://remplace-par-ton-url.fr/en/a_section/the_article/".to_string(),
        );
        let mut colocated_assets = AHashMap::new();
        colocated_assets.insert(
            "a_section/an_article/graph.jpg".to_string(),
            ("a_section/an_article/index.md".to_string(), "graph.jpg".to_string()),
        );

        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            permalinks,
            PathBuf::new(),
            colocated_assets,
        );
        let resolve = |path: &str, lang: &str| {
            let kwargs = Kwargs::from([
                ("path", tera::Value::from(path)),
                ("lang", tera::Value::from(lang)),
            ]);
            let ctx = Context::new();
            get_url.call(kwargs, &State::new(&ctx))
        };

        // Default language, then another language following that page's own slug
        assert_eq!(
            resolve("@/a_section/an_article/graph.jpg", "fr").unwrap(),
            "https://remplace-par-ton-url.fr/a_section/mon_article/graph.jpg"
        );
        assert_eq!(
            resolve("@/a_section/an_article/graph.jpg", "en").unwrap(),
            "https://remplace-par-ton-url.fr/en/a_section/the_article/graph.jpg"
        );
        // Unknown asset errors
        assert!(resolve("@/a_section/my-article/missing.jpg", "fr").is_err());
    }

    #[test]
    fn does_not_duplicate_lang() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let mut permalinks = HashMap::new();
        permalinks.insert(
            "a_section/a_page.md".to_string(),
            "https://remplace-par-ton-url.fr/a_section/a_page/".to_string(),
        );
        permalinks.insert(
            "a_section/a_page.en.md".to_string(),
            "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
        );
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            permalinks,
            PathBuf::new(),
            AHashMap::new(),
        );

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("/en/a_section/a_page/")),
            ("lang", tera::Value::from("en")),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "https://remplace-par-ton-url.fr/en/a_section/a_page"
        );
    }

    #[test]
    fn can_get_feed_urls_with_default_language() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config.clone(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        for feed_filename in &config.feed_filenames {
            let kwargs = Kwargs::from([
                ("path", tera::Value::from(feed_filename.as_str())),
                ("lang", tera::Value::from("fr")),
            ]);
            let ctx = Context::new();
            assert_eq!(
                get_url.call(kwargs, &State::new(&ctx)).unwrap(),
                "https://remplace-par-ton-url.fr/atom.xml"
            );
        }
    }

    #[test]
    fn can_get_feed_urls_with_other_language() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config.clone(),
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        for feed_filename in &config.feed_filenames {
            let kwargs = Kwargs::from([
                ("path", tera::Value::from(feed_filename.as_str())),
                ("lang", tera::Value::from("en")),
            ]);
            let ctx = Context::new();
            assert_eq!(
                get_url.call(kwargs, &State::new(&ctx)).unwrap(),
                "https://remplace-par-ton-url.fr/en/atom.xml"
            );
        }
    }

    #[test]
    fn can_get_file_hash_sha256_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("sha_type", tera::Value::from(256)),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"
        );
    }

    #[test]
    fn can_get_file_hash_sha256_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("sha_type", tera::Value::from(256)),
            ("base64", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "Vy5pHcaMP81lOuRjJhvbOPNdxvAXFdnOaHmTGd0ViEA="
        );
    }

    #[test]
    fn can_get_file_hash_sha384_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414"
        );
    }

    #[test]
    fn can_get_file_hash_sha384() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([("path", tera::Value::from("app.css"))]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "FBwJvSiJl3O3crvgZNi3GPodbyhSt+r9XtZonSa3SIO3ni6BTNadW1KrR2qihMQU"
        );
    }

    #[test]
    fn can_get_file_hash_sha512_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("sha_type", tera::Value::from(512)),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f"
        );
    }

    #[test]
    fn can_get_file_hash_sha512() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("app.css")),
            ("sha_type", tera::Value::from(512)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "N536s1EjuRWdnk6S3JDivkTPPC9/CbLi34Chshm0Yd41Vsk+GpzrMAjpmeLWpUtPHWXum+m+Y/pF7IiTFiM3Lw=="
        );
    }

    #[test]
    fn can_get_hash_sha256_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("literal", tera::Value::from("Hello World")),
            ("sha_type", tera::Value::from(256)),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
        );
    }

    #[test]
    fn can_get_hash_sha256_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("literal", tera::Value::from("Hello World")),
            ("sha_type", tera::Value::from(256)),
            ("base64", tera::Value::from(true)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4="
        );
    }

    #[test]
    fn can_get_hash_sha384_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("literal", tera::Value::from("Hello World")),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "99514329186b2f6ae4a1329e7ee6c610a729636335174ac6b740f9028396fcc803d0e93863a7c3d90f86beee782f4f3f"
        );
    }

    #[test]
    fn can_get_hash_sha384() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([("literal", tera::Value::from("Hello World"))]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "mVFDKRhrL2rkoTKefubGEKcpY2M1F0rGt0D5AoOW/MgD0Ok4Y6fD2Q+Gvu54L08/"
        );
    }

    #[test]
    fn can_get_hash_sha512_no_base64() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("literal", tera::Value::from("Hello World")),
            ("sha_type", tera::Value::from(512)),
            ("base64", tera::Value::from(false)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "2c74fd17edafd80e8447b0d46741ee243b7eb74dd2149a0ab1b9246fb30382f27e853d8585719e0e67cbda0daa8f51671064615d645ae27acb15bfb1447f459b"
        );
    }

    #[test]
    fn can_get_hash_sha512() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([
            ("literal", tera::Value::from("Hello World")),
            ("sha_type", tera::Value::from(512)),
        ]);
        let ctx = Context::new();
        assert_eq!(
            get_hash.call(kwargs, &State::new(&ctx)).unwrap(),
            "LHT9F+2v2A6ER7DUZ0HuJDt+t03SFJoKsbkkb7MDgvJ+hT2FhXGeDmfL2g2qj1FnEGRhXWRa4nrLFb+xRH9Fmw=="
        );
    }

    #[test]
    fn static_file_does_not_get_lang_prefix_from_context() {
        let config = Config::parse(CONFIG_DATA).unwrap();
        let dir = create_temp_dir();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );

        // lang="en" (non-default) comes from template context, not from kwarg
        let kwargs = Kwargs::from([("path", tera::Value::from("app.css"))]);
        let mut ctx = Context::new();
        ctx.insert("lang", "en");
        assert_eq!(
            get_url.call(kwargs, &State::new(&ctx)).unwrap(),
            "https://remplace-par-ton-url.fr/app.css"
        );
    }

    #[test]
    fn error_when_file_not_found_for_hash() {
        let dir = create_temp_dir();
        let get_hash = GetHash::new(dir.path().to_path_buf(), None, PathBuf::new());

        let kwargs = Kwargs::from([("path", tera::Value::from("doesnt-exist"))]);
        let ctx = Context::new();
        let err = get_hash.call(kwargs, &State::new(&ctx)).unwrap_err();
        assert!(err.to_string().contains("Cannot find file"));
    }

    // https://github.com/getzola/zola/issues/2621
    #[test]
    fn does_not_drop_lang() {
        let dir = create_temp_dir();
        let config = Config::parse(CONFIG_DATA).unwrap();
        let get_url = GetUrl::new(
            dir.path().to_path_buf(),
            config,
            HashMap::new(),
            PathBuf::new(),
            AHashMap::new(),
        );
        let kwargs = Kwargs::from([
            ("path", tera::Value::from("genres")),
            ("lang", tera::Value::from("en")),
        ]);
        let ctx = Context::new();
        let url = get_url.call(kwargs, &State::new(&ctx)).unwrap();
        assert_eq!(url, "https://remplace-par-ton-url.fr/en/genres");

        // https://github.com/getzola/zola/issues/2216
        let kwargs =
            Kwargs::from([("path", tera::Value::from("/")), ("lang", tera::Value::from("en"))]);
        let url = get_url.call(kwargs, &State::new(&ctx)).unwrap();
        assert_eq!(url, "https://remplace-par-ton-url.fr/en");

        let kwargs = Kwargs::from([
            ("path", tera::Value::from("/")),
            ("lang", tera::Value::from("en")),
            ("trailing_slash", tera::Value::from(true)),
        ]);
        let url = get_url.call(kwargs, &State::new(&ctx)).unwrap();
        assert_eq!(url, "https://remplace-par-ton-url.fr/en/");
    }
}
