use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;

use serde::Serialize;

use crate::context::Context;
use crate::decorators::{self, DecoratorDef};
#[cfg(feature = "script_helper")]
use crate::error::ScriptError;
use crate::error::{RenderError, TemplateError, TemplateFileError, TemplateRenderError};
use crate::helpers::{self, HelperDef};
use crate::output::{Output, StringOutput, WriteOutput};
use crate::render::{RenderContext, Renderable};
use crate::support::str::{self, StringWriter};
use crate::template::Template;

#[cfg(feature = "dir_source")]
use std::path;
#[cfg(feature = "dir_source")]
use walkdir::{DirEntry, WalkDir};

#[cfg(feature = "script_helper")]
use rhai::Engine;

#[cfg(feature = "script_helper")]
use crate::helpers::scripting::ScriptHelper;

/// This type represents an *escape fn*, that is a function whose purpose it is
/// to escape potentially problematic characters in a string.
///
/// An *escape fn* is represented as a `Box` to avoid unnecessary type
/// parameters (and because traits cannot be aliased using `type`).
pub type EscapeFn = Box<dyn Fn(&str) -> String + Send + Sync>;

/// The default *escape fn* replaces the characters `&"<>`
/// with the equivalent html / xml entities.
pub fn html_escape(data: &str) -> String {
    str::escape_html(data)
}

/// `EscapeFn` that does not change anything. Useful when using in a non-html
/// environment.
pub fn no_escape(data: &str) -> String {
    data.to_owned()
}

/// The single entry point of your Handlebars templates
///
/// It maintains compiled templates and registered helpers.
pub struct Registry<'reg> {
    templates: HashMap<String, Template>,
    helpers: HashMap<String, Box<dyn HelperDef + Send + Sync + 'reg>>,
    decorators: HashMap<String, Box<dyn DecoratorDef + Send + Sync + 'reg>>,
    escape_fn: EscapeFn,
    source_map: bool,
    strict_mode: bool,
    #[cfg(feature = "script_helper")]
    pub(crate) engine: Engine,
}

impl<'reg> Debug for Registry<'reg> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        f.debug_struct("Handlebars")
            .field("templates", &self.templates)
            .field("helpers", &self.helpers.keys())
            .field("decorators", &self.decorators.keys())
            .field("source_map", &self.source_map)
            .finish()
    }
}

impl<'reg> Default for Registry<'reg> {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "dir_source")]
fn filter_file(entry: &DirEntry, suffix: &str) -> bool {
    let path = entry.path();

    // ignore hidden files, emacs buffers and files with wrong suffix
    !path.is_file()
        || path
            .file_name()
            .map(|s| {
                let ds = s.to_string_lossy();
                ds.starts_with('.') || ds.starts_with('#') || !ds.ends_with(suffix)
            })
            .unwrap_or(true)
}

#[cfg(feature = "script_helper")]
fn rhai_engine() -> Engine {
    Engine::new()
}

impl<'reg> Registry<'reg> {
    pub fn new() -> Registry<'reg> {
        let r = Registry {
            templates: HashMap::new(),
            helpers: HashMap::new(),
            decorators: HashMap::new(),
            escape_fn: Box::new(html_escape),
            source_map: true,
            strict_mode: false,
            #[cfg(feature = "script_helper")]
            engine: rhai_engine(),
        };

        r.setup_builtins()
    }

    fn setup_builtins(mut self) -> Registry<'reg> {
        self.register_helper("if", Box::new(helpers::IF_HELPER));
        self.register_helper("unless", Box::new(helpers::UNLESS_HELPER));
        self.register_helper("each", Box::new(helpers::EACH_HELPER));
        self.register_helper("with", Box::new(helpers::WITH_HELPER));
        self.register_helper("lookup", Box::new(helpers::LOOKUP_HELPER));
        self.register_helper("raw", Box::new(helpers::RAW_HELPER));
        self.register_helper("log", Box::new(helpers::LOG_HELPER));

        self.register_helper("eq", Box::new(helpers::helper_boolean::eq));
        self.register_helper("ne", Box::new(helpers::helper_boolean::ne));
        self.register_helper("gt", Box::new(helpers::helper_boolean::gt));
        self.register_helper("gte", Box::new(helpers::helper_boolean::gte));
        self.register_helper("lt", Box::new(helpers::helper_boolean::lt));
        self.register_helper("lte", Box::new(helpers::helper_boolean::lte));
        self.register_helper("and", Box::new(helpers::helper_boolean::and));
        self.register_helper("or", Box::new(helpers::helper_boolean::or));
        self.register_helper("not", Box::new(helpers::helper_boolean::not));

        self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR));
        self
    }

    /// Enable handlebars template source map
    ///
    /// Source map provides line/col reporting on error. It uses slightly
    /// more memory to maintain the data.
    ///
    /// Default is true.
    pub fn source_map_enabled(&mut self, enable: bool) {
        self.source_map = enable;
    }

    /// Enable handlebars strict mode
    ///
    /// By default, handlebars renders empty string for value that
    /// undefined or never exists. Since rust is a static type
    /// language, we offer strict mode in handlebars-rust.  In strict
    /// mode, if you were to render a value that doesn't exist, a
    /// `RenderError` will be raised.
    pub fn set_strict_mode(&mut self, enable: bool) {
        self.strict_mode = enable;
    }

    /// Return strict mode state, default is false.
    ///
    /// By default, handlebars renders empty string for value that
    /// undefined or never exists. Since rust is a static type
    /// language, we offer strict mode in handlebars-rust.  In strict
    /// mode, if you were access a value that doesn't exist, a
    /// `RenderError` will be raised.
    pub fn strict_mode(&self) -> bool {
        self.strict_mode
    }

    /// Register a `Template`
    ///
    /// This is infallible since the template has already been parsed and
    /// insert cannot fail. If there is an existing template with this name it
    /// will be overwritten.
    pub fn register_template(&mut self, name: &str, tpl: Template) {
        self.templates.insert(name.to_string(), tpl);
    }

    /// Register a template string
    ///
    /// Returns `TemplateError` if there is syntax error on parsing the template.
    pub fn register_template_string<S>(
        &mut self,
        name: &str,
        tpl_str: S,
    ) -> Result<(), TemplateError>
    where
        S: AsRef<str>,
    {
        let template = Template::compile_with_name(tpl_str, name.to_owned(), self.source_map)?;
        self.register_template(name, template);
        Ok(())
    }

    /// Register a partial string
    ///
    /// A named partial will be added to the registry. It will overwrite template with
    /// same name. Currently a registered partial is just identical to a template.
    pub fn register_partial<S>(&mut self, name: &str, partial_str: S) -> Result<(), TemplateError>
    where
        S: AsRef<str>,
    {
        self.register_template_string(name, partial_str)
    }

    /// Register a template from a path
    pub fn register_template_file<P>(
        &mut self,
        name: &str,
        tpl_path: P,
    ) -> Result<(), TemplateFileError>
    where
        P: AsRef<Path>,
    {
        let mut reader = BufReader::new(
            File::open(tpl_path).map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?,
        );
        self.register_template_source(name, &mut reader)
    }

    /// Register templates from a directory
    ///
    /// * `tpl_extension`: the template file extension
    /// * `dir_path`: the path of directory
    ///
    /// Hidden files and tempfile (starts with `#`) will be ignored. All registered
    /// will use their relative name as template name. For example, when `dir_path` is
    /// `templates/` and `tpl_extension` is `.hbs`, the file
    /// `templates/some/path/file.hbs` will be registered as `some/path/file`.
    ///
    /// This method is not available by default.
    /// You will need to enable the `dir_source` feature to use it.
    #[cfg(feature = "dir_source")]
    pub fn register_templates_directory<P>(
        &mut self,
        tpl_extension: &'static str,
        dir_path: P,
    ) -> Result<(), TemplateFileError>
    where
        P: AsRef<Path>,
    {
        let dir_path = dir_path.as_ref();

        let prefix_len = if dir_path
            .to_string_lossy()
            .ends_with(|c| c == '\\' || c == '/')
        // `/` will work on windows too so we still need to check
        {
            dir_path.to_string_lossy().len()
        } else {
            dir_path.to_string_lossy().len() + 1
        };

        let walker = WalkDir::new(dir_path);
        let dir_iter = walker
            .min_depth(1)
            .into_iter()
            .filter(|e| e.is_ok() && !filter_file(e.as_ref().unwrap(), tpl_extension));

        for entry in dir_iter {
            let entry = entry?;

            let tpl_path = entry.path();
            let tpl_file_path = entry.path().to_string_lossy();

            let tpl_name = &tpl_file_path[prefix_len..tpl_file_path.len() - tpl_extension.len()];
            // replace platform path separator with our internal one
            let tpl_canonical_name = tpl_name.replace(path::MAIN_SEPARATOR, "/");
            self.register_template_file(&tpl_canonical_name, &tpl_path)?;
        }

        Ok(())
    }

    /// Register a template from `std::io::Read` source
    pub fn register_template_source<R>(
        &mut self,
        name: &str,
        tpl_source: &mut R,
    ) -> Result<(), TemplateFileError>
    where
        R: Read,
    {
        let mut buf = String::new();
        tpl_source
            .read_to_string(&mut buf)
            .map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?;
        self.register_template_string(name, buf)?;
        Ok(())
    }

    /// Remove a template from the registry
    pub fn unregister_template(&mut self, name: &str) {
        self.templates.remove(name);
    }

    /// Register a helper
    pub fn register_helper(
        &mut self,
        name: &str,
        def: Box<dyn HelperDef + Send + Sync + 'reg>,
    ) -> Option<Box<dyn HelperDef + Send + Sync + 'reg>> {
        self.helpers.insert(name.to_string(), def)
    }

    /// Register a [rhai](https://docs.rs/rhai/) script as handlebars helper
    ///
    /// Currently only simple helpers are supported. You can do computation or
    /// string formatting with rhai script.
    ///
    /// Helper parameters and hash are available in rhai script as array `params`
    /// and map `hash`. Example script:
    ///
    /// ```handlebars
    /// {{percent 0.34 label="%"}}
    /// ```
    ///
    /// ```rhai
    /// // percent.rhai
    /// let value = params[0];
    /// let label = hash["label"];
    ///
    /// (value * 100).to_string() + label
    /// ```
    ///
    ///
    #[cfg(feature = "script_helper")]
    pub fn register_script_helper(
        &mut self,
        name: &str,
        script: String,
    ) -> Result<Option<Box<dyn HelperDef + Send + Sync + 'reg>>, ScriptError> {
        let compiled = self.engine.compile(&script)?;
        let script_helper = ScriptHelper { script: compiled };
        Ok(self
            .helpers
            .insert(name.to_string(), Box::new(script_helper)))
    }

    /// Register a [rhai](https://docs.rs/rhai/) script from file
    #[cfg(feature = "script_helper")]
    pub fn register_script_helper_file<P>(
        &mut self,
        name: &str,
        script_path: P,
    ) -> Result<Option<Box<dyn HelperDef + Send + Sync + 'reg>>, ScriptError>
    where
        P: AsRef<Path>,
    {
        let mut script = String::new();
        {
            let mut file = File::open(script_path)?;
            file.read_to_string(&mut script)?;
        }

        self.register_script_helper(name, script)
    }

    /// Register a decorator
    pub fn register_decorator(
        &mut self,
        name: &str,
        def: Box<dyn DecoratorDef + Send + Sync + 'reg>,
    ) -> Option<Box<dyn DecoratorDef + Send + Sync + 'reg>> {
        self.decorators.insert(name.to_string(), def)
    }

    /// Register a new *escape fn* to be used from now on by this registry.
    pub fn register_escape_fn<F: 'static + Fn(&str) -> String + Send + Sync>(
        &mut self,
        escape_fn: F,
    ) {
        self.escape_fn = Box::new(escape_fn);
    }

    /// Restore the default *escape fn*.
    pub fn unregister_escape_fn(&mut self) {
        self.escape_fn = Box::new(html_escape);
    }

    /// Get a reference to the current *escape fn*.
    pub fn get_escape_fn(&self) -> &dyn Fn(&str) -> String {
        &*self.escape_fn
    }

    /// Return `true` if a template is registered for the given name
    pub fn has_template(&self, name: &str) -> bool {
        self.get_template(name).is_some()
    }

    /// Return a registered template,
    pub fn get_template(&self, name: &str) -> Option<&Template> {
        self.templates.get(name)
    }

    /// Return a registered helper
    pub fn get_helper(&self, name: &str) -> Option<&(dyn HelperDef + Send + Sync + 'reg)> {
        self.helpers.get(name).map(|v| v.as_ref())
    }

    #[inline]
    pub(crate) fn has_helper(&self, name: &str) -> bool {
        self.helpers.contains_key(name)
    }

    /// Return a registered decorator
    pub fn get_decorator(&self, name: &str) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> {
        self.decorators.get(name).map(|v| v.as_ref())
    }

    /// Return all templates registered
    pub fn get_templates(&self) -> &HashMap<String, Template> {
        &self.templates
    }

    /// Unregister all templates
    pub fn clear_templates(&mut self) {
        self.templates.clear();
    }

    fn render_to_output<O>(
        &self,
        name: &str,
        ctx: &Context,
        output: &mut O,
    ) -> Result<(), RenderError>
    where
        O: Output,
    {
        self.get_template(name)
            .ok_or_else(|| RenderError::new(format!("Template not found: {}", name)))
            .and_then(|t| {
                let mut render_context = RenderContext::new(t.name.as_ref());
                t.render(self, &ctx, &mut render_context, output)
            })
    }

    /// Render a registered template with some data into a string
    ///
    /// * `name` is the template name you registered previously
    /// * `data` is the data that implements `serde::Serialize`
    ///
    /// Returns rendered string or a struct with error information
    pub fn render<T>(&self, name: &str, data: &T) -> Result<String, RenderError>
    where
        T: Serialize,
    {
        let mut output = StringOutput::new();
        let ctx = Context::wraps(&data)?;
        self.render_to_output(name, &ctx, &mut output)?;
        output.into_string().map_err(RenderError::from)
    }

    /// Render a registered template with reused context
    pub fn render_with_context(&self, name: &str, ctx: &Context) -> Result<String, RenderError> {
        let mut output = StringOutput::new();
        self.render_to_output(name, ctx, &mut output)?;
        output.into_string().map_err(RenderError::from)
    }

    /// Render a registered template and write some data to the `std::io::Write`
    pub fn render_to_write<T, W>(&self, name: &str, data: &T, writer: W) -> Result<(), RenderError>
    where
        T: Serialize,
        W: Write,
    {
        let mut output = WriteOutput::new(writer);
        let ctx = Context::wraps(data)?;
        self.render_to_output(name, &ctx, &mut output)
    }

    /// Render a template string using current registry without registering it
    pub fn render_template<T>(
        &self,
        template_string: &str,
        data: &T,
    ) -> Result<String, TemplateRenderError>
    where
        T: Serialize,
    {
        let mut writer = StringWriter::new();
        self.render_template_to_write(template_string, data, &mut writer)?;
        Ok(writer.into_string())
    }

    /// Render a template string using reused context data
    pub fn render_template_with_context(
        &self,
        template_string: &str,
        ctx: &Context,
    ) -> Result<String, TemplateRenderError> {
        let tpl = Template::compile2(template_string, self.source_map)?;

        let mut out = StringOutput::new();
        {
            let mut render_context = RenderContext::new(None);
            tpl.render(self, &ctx, &mut render_context, &mut out)?;
        }

        out.into_string()
            .map_err(|e| TemplateRenderError::from(RenderError::from(e)))
    }

    /// Render a template string using current registry without registering it
    pub fn render_template_to_write<T, W>(
        &self,
        template_string: &str,
        data: &T,
        writer: W,
    ) -> Result<(), TemplateRenderError>
    where
        T: Serialize,
        W: Write,
    {
        let tpl = Template::compile2(template_string, self.source_map)?;
        let ctx = Context::wraps(data)?;
        let mut render_context = RenderContext::new(None);
        let mut out = WriteOutput::new(writer);
        tpl.render(self, &ctx, &mut render_context, &mut out)
            .map_err(TemplateRenderError::from)
    }

    /// Render a template source using current registry without registering it
    pub fn render_template_source_to_write<T, R, W>(
        &self,
        template_source: &mut R,
        data: &T,
        writer: W,
    ) -> Result<(), TemplateRenderError>
    where
        T: Serialize,
        W: Write,
        R: Read,
    {
        let mut tpl_str = String::new();
        template_source
            .read_to_string(&mut tpl_str)
            .map_err(|e| TemplateRenderError::IOError(e, "Unnamed template source".to_owned()))?;
        self.render_template_to_write(&tpl_str, data, writer)
    }
}

#[cfg(test)]
mod test {
    use crate::context::Context;
    use crate::error::RenderError;
    use crate::helpers::HelperDef;
    use crate::output::Output;
    use crate::registry::Registry;
    use crate::render::{Helper, RenderContext, Renderable};
    use crate::support::str::StringWriter;
    use crate::template::Template;
    #[cfg(feature = "dir_source")]
    use std::fs::{DirBuilder, File};
    #[cfg(feature = "dir_source")]
    use std::io::Write;
    #[cfg(feature = "dir_source")]
    use tempfile::tempdir;

    #[derive(Clone, Copy)]
    struct DummyHelper;

    impl HelperDef for DummyHelper {
        fn call<'reg: 'rc, 'rc>(
            &self,
            h: &Helper<'reg, 'rc>,
            r: &'reg Registry<'reg>,
            ctx: &'rc Context,
            rc: &mut RenderContext<'reg, 'rc>,
            out: &mut dyn Output,
        ) -> Result<(), RenderError> {
            h.template().unwrap().render(r, ctx, rc, out)
        }
    }

    static DUMMY_HELPER: DummyHelper = DummyHelper;

    #[test]
    fn test_registry_operations() {
        let mut r = Registry::new();

        assert!(r.register_template_string("index", "<h1></h1>").is_ok());

        let tpl = Template::compile("<h2></h2>").unwrap();
        r.register_template("index2", tpl);

        assert_eq!(r.templates.len(), 2);

        r.unregister_template("index");
        assert_eq!(r.templates.len(), 1);

        r.clear_templates();
        assert_eq!(r.templates.len(), 0);

        r.register_helper("dummy", Box::new(DUMMY_HELPER));

        // built-in helpers plus 1
        let num_helpers = 7;
        let num_boolean_helpers = 9; // stuff like gt and lte
        let num_custom_helpers = 1; // dummy from above
        assert_eq!(
            r.helpers.len(),
            num_helpers + num_boolean_helpers + num_custom_helpers
        );
    }

    #[test]
    #[cfg(feature = "dir_source")]
    fn test_register_templates_directory() {
        let mut r = Registry::new();
        {
            let dir = tempdir().unwrap();

            assert_eq!(r.templates.len(), 0);

            let file1_path = dir.path().join("t1.hbs");
            let mut file1: File = File::create(&file1_path).unwrap();
            writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();

            let file2_path = dir.path().join("t2.hbs");
            let mut file2: File = File::create(&file2_path).unwrap();
            writeln!(file2, "<h1>Hola {{world}}!</h1>").unwrap();

            let file3_path = dir.path().join("t3.hbs");
            let mut file3: File = File::create(&file3_path).unwrap();
            writeln!(file3, "<h1>Hallo {{world}}!</h1>").unwrap();

            let file4_path = dir.path().join(".t4.hbs");
            let mut file4: File = File::create(&file4_path).unwrap();
            writeln!(file4, "<h1>Hallo {{world}}!</h1>").unwrap();

            r.register_templates_directory(".hbs", dir.path()).unwrap();

            assert_eq!(r.templates.len(), 3);
            assert_eq!(r.templates.contains_key("t1"), true);
            assert_eq!(r.templates.contains_key("t2"), true);
            assert_eq!(r.templates.contains_key("t3"), true);
            assert_eq!(r.templates.contains_key("t4"), false);

            drop(file1);
            drop(file2);
            drop(file3);

            dir.close().unwrap();
        }

        {
            let dir = tempdir().unwrap();

            let file1_path = dir.path().join("t4.hbs");
            let mut file1: File = File::create(&file1_path).unwrap();
            writeln!(file1, "<h1>Hello {{world}}!</h1>").unwrap();

            let file2_path = dir.path().join("t5.erb");
            let mut file2: File = File::create(&file2_path).unwrap();
            writeln!(file2, "<h1>Hello {{% world %}}!</h1>").unwrap();

            let file3_path = dir.path().join("t6.html");
            let mut file3: File = File::create(&file3_path).unwrap();
            writeln!(file3, "<h1>Hello world!</h1>").unwrap();

            r.register_templates_directory(".hbs", dir.path()).unwrap();

            assert_eq!(r.templates.len(), 4);
            assert_eq!(r.templates.contains_key("t4"), true);

            drop(file1);
            drop(file2);
            drop(file3);

            dir.close().unwrap();
        }

        {
            let dir = tempdir().unwrap();

            let _ = DirBuilder::new().create(dir.path().join("french")).unwrap();
            let _ = DirBuilder::new()
                .create(dir.path().join("portugese"))
                .unwrap();
            let _ = DirBuilder::new()
                .create(dir.path().join("italian"))
                .unwrap();

            let file1_path = dir.path().join("french/t7.hbs");
            let mut file1: File = File::create(&file1_path).unwrap();
            writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();

            let file2_path = dir.path().join("portugese/t8.hbs");
            let mut file2: File = File::create(&file2_path).unwrap();
            writeln!(file2, "<h1>Ola {{world}}!</h1>").unwrap();

            let file3_path = dir.path().join("italian/t9.hbs");
            let mut file3: File = File::create(&file3_path).unwrap();
            writeln!(file3, "<h1>Ciao {{world}}!</h1>").unwrap();

            r.register_templates_directory(".hbs", dir.path()).unwrap();

            assert_eq!(r.templates.len(), 7);
            assert_eq!(r.templates.contains_key("french/t7"), true);
            assert_eq!(r.templates.contains_key("portugese/t8"), true);
            assert_eq!(r.templates.contains_key("italian/t9"), true);

            drop(file1);
            drop(file2);
            drop(file3);

            dir.close().unwrap();
        }

        {
            let dir = tempdir().unwrap();

            let file1_path = dir.path().join("t10.hbs");
            let mut file1: File = File::create(&file1_path).unwrap();
            writeln!(file1, "<h1>Bonjour {{world}}!</h1>").unwrap();

            let mut dir_path = dir
                .path()
                .to_string_lossy()
                .replace(std::path::MAIN_SEPARATOR, "/");
            if !dir_path.ends_with("/") {
                dir_path.push('/');
            }
            r.register_templates_directory(".hbs", dir_path).unwrap();

            assert_eq!(r.templates.len(), 8);
            assert_eq!(r.templates.contains_key("t10"), true);

            drop(file1);
            dir.close().unwrap();
        }
    }

    #[test]
    fn test_render_to_write() {
        let mut r = Registry::new();

        assert!(r.register_template_string("index", "<h1></h1>").is_ok());

        let mut sw = StringWriter::new();
        {
            r.render_to_write("index", &(), &mut sw).ok().unwrap();
        }

        assert_eq!("<h1></h1>".to_string(), sw.into_string());
    }

    #[test]
    fn test_escape_fn() {
        let mut r = Registry::new();

        let input = String::from("\"<>&");

        r.register_template_string("test", String::from("{{this}}"))
            .unwrap();

        assert_eq!("&quot;&lt;&gt;&amp;", r.render("test", &input).unwrap());

        r.register_escape_fn(|s| s.into());

        assert_eq!("\"<>&", r.render("test", &input).unwrap());

        r.unregister_escape_fn();

        assert_eq!("&quot;&lt;&gt;&amp;", r.render("test", &input).unwrap());
    }

    #[test]
    fn test_escape() {
        let r = Registry::new();
        let data = json!({"hello": "world"});

        assert_eq!(
            "{{hello}}",
            r.render_template(r"\{{hello}}", &data).unwrap()
        );

        assert_eq!(
            " {{hello}}",
            r.render_template(r" \{{hello}}", &data).unwrap()
        );

        assert_eq!(r"\world", r.render_template(r"\\{{hello}}", &data).unwrap());
    }

    #[test]
    fn test_strict_mode() {
        let mut r = Registry::new();
        assert!(!r.strict_mode());

        r.set_strict_mode(true);
        assert!(r.strict_mode());

        let data = json!({
            "the_only_key": "the_only_value"
        });

        assert!(r
            .render_template("accessing the_only_key {{the_only_key}}", &data)
            .is_ok());
        assert!(r
            .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
            .is_err());

        let render_error = r
            .render_template("accessing non-exists key {{the_key_never_exists}}", &data)
            .unwrap_err();
        assert_eq!(
            render_error.as_render_error().unwrap().column_no.unwrap(),
            26
        );

        let data2 = json!([1, 2, 3]);
        assert!(r
            .render_template("accessing valid array index {{this.[2]}}", &data2)
            .is_ok());
        assert!(r
            .render_template("accessing invalid array index {{this.[3]}}", &data2)
            .is_err());
        let render_error2 = r
            .render_template("accessing invalid array index {{this.[3]}}", &data2)
            .unwrap_err();
        assert_eq!(
            render_error2.as_render_error().unwrap().column_no.unwrap(),
            31
        );
    }

    use crate::json::value::ScopedJson;
    struct GenMissingHelper;
    impl HelperDef for GenMissingHelper {
        fn call_inner<'reg: 'rc, 'rc>(
            &self,
            _: &Helper<'reg, 'rc>,
            _: &'reg Registry<'reg>,
            _: &'rc Context,
            _: &mut RenderContext<'reg, 'rc>,
        ) -> Result<Option<ScopedJson<'reg, 'rc>>, RenderError> {
            Ok(Some(ScopedJson::Missing))
        }
    }

    #[test]
    fn test_strict_mode_in_helper() {
        let mut r = Registry::new();
        r.set_strict_mode(true);

        r.register_helper(
            "check_missing",
            Box::new(
                |h: &Helper<'_, '_>,
                 _: &Registry<'_>,
                 _: &Context,
                 _: &mut RenderContext<'_, '_>,
                 _: &mut dyn Output|
                 -> Result<(), RenderError> {
                    let value = h.param(0).unwrap();
                    assert!(value.is_value_missing());
                    Ok(())
                },
            ),
        );

        r.register_helper("generate_missing_value", Box::new(GenMissingHelper));

        let data = json!({
            "the_key_we_have": "the_value_we_have"
        });
        assert!(r
            .render_template("accessing non-exists key {{the_key_we_dont_have}}", &data)
            .is_err());
        assert!(r
            .render_template(
                "accessing non-exists key from helper {{check_missing the_key_we_dont_have}}",
                &data
            )
            .is_ok());
        assert!(r
            .render_template(
                "accessing helper that generates missing value {{generate_missing_value}}",
                &data
            )
            .is_err());
    }

    #[test]
    fn test_html_expression() {
        let reg = Registry::new();
        assert_eq!(
            reg.render_template("{{{ a }}}", &json!({"a": "<b>bold</b>"}))
                .unwrap(),
            "<b>bold</b>"
        );
        assert_eq!(
            reg.render_template("{{ &a }}", &json!({"a": "<b>bold</b>"}))
                .unwrap(),
            "<b>bold</b>"
        );
    }

    #[test]
    fn test_render_context() {
        let mut reg = Registry::new();

        let data = json!([0, 1, 2, 3]);

        assert_eq!(
            "0123",
            reg.render_template_with_context(
                "{{#each this}}{{this}}{{/each}}",
                &Context::wraps(&data).unwrap()
            )
            .unwrap()
        );

        reg.register_template_string("t0", "{{#each this}}{{this}}{{/each}}")
            .unwrap();
        assert_eq!(
            "0123",
            reg.render_with_context("t0", &Context::wraps(&data).unwrap())
                .unwrap()
        );
    }

    #[test]
    fn test_keys_starts_with_null() {
        env_logger::init();
        let reg = Registry::new();
        let data = json!({
            "optional": true,
            "is_null": true,
            "nullable": true,
            "null": true,
            "falsevalue": true,
        });
        assert_eq!(
            "optional: true --> true",
            reg.render_template(
                "optional: {{optional}} --> {{#if optional }}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
        assert_eq!(
            "is_null: true --> true",
            reg.render_template(
                "is_null: {{is_null}} --> {{#if is_null }}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
        assert_eq!(
            "nullable: true --> true",
            reg.render_template(
                "nullable: {{nullable}} --> {{#if nullable }}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
        assert_eq!(
            "falsevalue: true --> true",
            reg.render_template(
                "falsevalue: {{falsevalue}} --> {{#if falsevalue }}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
        assert_eq!(
            "null: true --> false",
            reg.render_template(
                "null: {{null}} --> {{#if null }}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
        assert_eq!(
            "null: true --> true",
            reg.render_template(
                "null: {{null}} --> {{#if this.[null]}}true{{else}}false{{/if}}",
                &data
            )
            .unwrap()
        );
    }
}
