//! Mdbook's configuration system.
//!
//! The main entrypoint of the `config` module is the `Config` struct. This acts
//! essentially as a bag of configuration information, with a couple
//! pre-determined tables ([`BookConfig`] and [`BuildConfig`]) as well as support
//! for arbitrary data which is exposed to plugins and alternative backends.
//!
//!
//! # Examples
//!
//! ```rust
//! # use anyhow::Result;
//! use std::path::PathBuf;
//! use std::str::FromStr;
//! use mdbook_core::config::Config;
//!
//! # fn run() -> Result<()> {
//! let src = r#"
//! [book]
//! title = "My Book"
//! authors = ["Michael-F-Bryan"]
//!
//! [preprocessor.my-preprocessor]
//! bar = 123
//! "#;
//!
//! // load the `Config` from a toml string
//! let mut cfg = Config::from_str(src)?;
//!
//! // retrieve a nested value
//! let bar = cfg.get::<i32>("preprocessor.my-preprocessor.bar")?;
//! assert_eq!(bar, Some(123));
//!
//! // Set the `output.html.theme` directory
//! assert!(cfg.get::<toml::Value>("output.html")?.is_none());
//! cfg.set("output.html.theme", "./themes");
//!
//! // then load it again, automatically deserializing to a `PathBuf`.
//! let got = cfg.get("output.html.theme")?;
//! assert_eq!(got, Some(PathBuf::from("./themes")));
//! # Ok(())
//! # }
//! # run().unwrap()
//! ```

use crate::static_regex;
use crate::utils::{TomlExt, fs, log_backtrace};
use anyhow::{Context, Error, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml::Value;
use toml::value::Table;
use tracing::{debug, trace};

/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[non_exhaustive]
pub struct Config {
    /// Metadata about the book.
    pub book: BookConfig,
    /// Information about the build environment.
    #[serde(skip_serializing_if = "is_default")]
    pub build: BuildConfig,
    /// Information about Rust language support.
    #[serde(skip_serializing_if = "is_default")]
    pub rust: RustConfig,
    /// The renderer configurations.
    #[serde(skip_serializing_if = "toml_is_empty")]
    output: Value,
    /// The preprocessor configurations.
    #[serde(skip_serializing_if = "toml_is_empty")]
    preprocessor: Value,
}

/// Helper for serde serialization.
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
    t == &T::default()
}

/// Helper for serde serialization.
fn toml_is_empty(table: &Value) -> bool {
    table.as_table().unwrap().is_empty()
}

impl FromStr for Config {
    type Err = Error;

    /// Load a `Config` from some string.
    fn from_str(src: &str) -> Result<Self> {
        toml::from_str(src).with_context(|| "Invalid configuration file")
    }
}

impl Default for Config {
    fn default() -> Config {
        Config {
            book: BookConfig::default(),
            build: BuildConfig::default(),
            rust: RustConfig::default(),
            output: Value::Table(Table::default()),
            preprocessor: Value::Table(Table::default()),
        }
    }
}

impl Config {
    /// Load the configuration file from disk.
    pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
        let cfg = fs::read_to_string(config_file)?;
        Config::from_str(&cfg)
    }

    /// Updates the `Config` from the available environment variables.
    ///
    /// Variables starting with `MDBOOK_` are used for configuration. The key is
    /// created by removing the `MDBOOK_` prefix and turning the resulting
    /// string into `kebab-case`. Double underscores (`__`) separate nested
    /// keys, while a single underscore (`_`) is replaced with a dash (`-`).
    ///
    /// For example:
    ///
    /// - `MDBOOK_book` -> `book`
    /// - `MDBOOK_BOOK` -> `book`
    /// - `MDBOOK_BOOK__TITLE` -> `book.title`
    /// - `MDBOOK_BOOK__TEXT_DIRECTION` -> `book.text-direction`
    ///
    /// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
    /// override the book's title without needing to touch your `book.toml`.
    ///
    /// > **Note:** To facilitate setting more complex config items, the value
    /// > of an environment variable is first parsed as JSON, falling back to a
    /// > string if the parse fails.
    /// >
    /// > This means, if you so desired, you could override all book metadata
    /// > when building the book with something like
    /// >
    /// > ```text
    /// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
    /// > $ mdbook build
    /// > ```
    ///
    /// The latter case may be useful in situations where `mdbook` is invoked
    /// from a script or CI, where it sometimes isn't possible to update the
    /// `book.toml` before building.
    pub fn update_from_env(&mut self) -> Result<()> {
        debug!("Updating the config from environment variables");

        static_regex!(
            VALID_KEY,
            r"^(:?book|build|rust|output|preprocessor)(:?$|\.)"
        );

        let overrides =
            env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));

        for (key, value) in overrides {
            trace!("{} => {}", key, value);
            if !VALID_KEY.is_match(&key) {
                // Ignore environment variables for other top-level things.
                // This allows users to set things like `MDBOOK_VERSION` or
                // `MDBOOK_DOWNLOAD_URL` for their own scripts and not
                // interfere with how the config is loaded.
                continue;
            }
            let parsed_value = serde_json::from_str(&value)
                .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));

            self.set(key, parsed_value)?;
        }
        Ok(())
    }

    /// Get a value from the configuration.
    ///
    /// This fetches a value from the book configuration. The key can have
    /// dotted indices to access nested items (e.g. `output.html.playground`
    /// will fetch the "playground" out of the html output table).
    ///
    /// This can only access the `output` and `preprocessor` tables.
    ///
    /// Returns `Ok(None)` if the field is not set.
    ///
    /// Returns `Err` if it fails to deserialize.
    pub fn get<'de, T: Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
        let (key, table) = if let Some(key) = name.strip_prefix("output.") {
            (key, &self.output)
        } else if let Some(key) = name.strip_prefix("preprocessor.") {
            (key, &self.preprocessor)
        } else {
            bail!(
                "unable to get `{name}`, only `output` and `preprocessor` table entries are allowed"
            );
        };
        table
            .read(key)
            .map(|value| {
                value
                    .clone()
                    .try_into()
                    .with_context(|| format!("Failed to deserialize `{name}`"))
            })
            .transpose()
    }

    /// Returns whether the config contains the given dotted key name.
    ///
    /// The key can have dotted indices to access nested items (e.g.
    /// `preprocessor.foo.bar` will check if that key is set in the config).
    ///
    /// This can only access the `output` and `preprocessor` tables.
    pub fn contains_key(&self, name: &str) -> bool {
        if let Some(key) = name.strip_prefix("output.") {
            self.output.read(key)
        } else if let Some(key) = name.strip_prefix("preprocessor.") {
            self.preprocessor.read(key)
        } else {
            panic!("invalid key `{name}`");
        }
        .is_some()
    }

    /// Returns the configuration for all preprocessors.
    pub fn preprocessors<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
        self.preprocessor
            .clone()
            .try_into()
            .with_context(|| "Failed to read preprocessors")
    }

    /// Returns the configuration for all renderers.
    pub fn outputs<'de, T: Deserialize<'de>>(&self) -> Result<BTreeMap<String, T>> {
        self.output
            .clone()
            .try_into()
            .with_context(|| "Failed to read renderers")
    }

    /// Convenience method for getting the html renderer's configuration.
    ///
    /// # Note
    ///
    /// This is for compatibility only. It will be removed completely once the
    /// HTML renderer is refactored to be less coupled to `mdbook` internals.
    #[doc(hidden)]
    pub fn html_config(&self) -> Option<HtmlConfig> {
        match self.get("output.html") {
            Ok(Some(config)) => Some(config),
            Ok(None) => None,
            Err(e) => {
                log_backtrace(&e);
                None
            }
        }
    }

    /// Set a config key, clobbering any existing values along the way.
    ///
    /// The key can have dotted indices for nested items (e.g.
    /// `output.html.playground` will set the "playground" in the html output
    /// table).
    ///
    /// # Errors
    ///
    /// This will fail if:
    ///
    /// - The value cannot be represented as TOML.
    /// - The value is not a correct type.
    /// - The key is an unknown configuration option.
    pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
        let index = index.as_ref();

        let value = Value::try_from(value)
            .with_context(|| "Unable to represent the item as a JSON Value")?;

        if index == "book" {
            self.book = value.try_into()?;
        } else if index == "build" {
            self.build = value.try_into()?;
        } else if index == "rust" {
            self.rust = value.try_into()?;
        } else if index == "output" {
            self.output = value;
        } else if index == "preprocessor" {
            self.preprocessor = value;
        } else if let Some(key) = index.strip_prefix("book.") {
            self.book.update_value(key, value)?;
        } else if let Some(key) = index.strip_prefix("build.") {
            self.build.update_value(key, value)?;
        } else if let Some(key) = index.strip_prefix("rust.") {
            self.rust.update_value(key, value)?;
        } else if let Some(key) = index.strip_prefix("output.") {
            self.output.update_value(key, value)?;
        } else if let Some(key) = index.strip_prefix("preprocessor.") {
            self.preprocessor.update_value(key, value)?;
        } else {
            bail!("invalid key `{index}`");
        }

        Ok(())
    }
}

fn parse_env(key: &str) -> Option<String> {
    key.strip_prefix("MDBOOK_")
        .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
}

/// Configuration options which are specific to the book and required for
/// loading it from disk.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct BookConfig {
    /// The book's title.
    pub title: Option<String>,
    /// The book's authors.
    pub authors: Vec<String>,
    /// An optional description for the book.
    pub description: Option<String>,
    /// Location of the book source relative to the book's root directory.
    #[serde(skip_serializing_if = "is_default_src")]
    pub src: PathBuf,
    /// The main language of the book.
    pub language: Option<String>,
    /// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL).
    /// When not specified, the text direction is derived from [`BookConfig::language`].
    pub text_direction: Option<TextDirection>,
}

/// Helper for serde serialization.
fn is_default_src(src: &PathBuf) -> bool {
    src == Path::new("src")
}

impl Default for BookConfig {
    fn default() -> BookConfig {
        BookConfig {
            title: None,
            authors: Vec::new(),
            description: None,
            src: PathBuf::from("src"),
            language: Some(String::from("en")),
            text_direction: None,
        }
    }
}

impl BookConfig {
    /// Gets the realized text direction, either from [`BookConfig::text_direction`]
    /// or derived from [`BookConfig::language`], to be used by templating engines.
    pub fn realized_text_direction(&self) -> TextDirection {
        if let Some(direction) = self.text_direction {
            direction
        } else {
            TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
        }
    }
}

/// Text direction to use for HTML output
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TextDirection {
    /// Left to right.
    #[serde(rename = "ltr")]
    LeftToRight,
    /// Right to left
    #[serde(rename = "rtl")]
    RightToLeft,
}

impl TextDirection {
    /// Gets the text direction from language code
    pub fn from_lang_code(code: &str) -> Self {
        match code {
            // list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16
            "ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
            | "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
            | "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
            _ => TextDirection::LeftToRight,
        }
    }
}

/// Configuration for the build procedure.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct BuildConfig {
    /// Where to put built artifacts relative to the book's root directory.
    pub build_dir: PathBuf,
    /// Should non-existent markdown files specified in `SUMMARY.md` be created
    /// if they don't exist?
    pub create_missing: bool,
    /// Should the default preprocessors always be used when they are
    /// compatible with the renderer?
    pub use_default_preprocessors: bool,
    /// Extra directories to trigger rebuild when watching/serving
    pub extra_watch_dirs: Vec<PathBuf>,
}

impl Default for BuildConfig {
    fn default() -> BuildConfig {
        BuildConfig {
            build_dir: PathBuf::from("book"),
            create_missing: true,
            use_default_preprocessors: true,
            extra_watch_dirs: Vec::new(),
        }
    }
}

/// Configuration for the Rust compiler(e.g., for playground)
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct RustConfig {
    /// Rust edition used in playground
    pub edition: Option<RustEdition>,
}

/// Rust edition to use for the code.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RustEdition {
    /// The 2024 edition of Rust
    #[serde(rename = "2024")]
    E2024,
    /// The 2021 edition of Rust
    #[serde(rename = "2021")]
    E2021,
    /// The 2018 edition of Rust
    #[serde(rename = "2018")]
    E2018,
    /// The 2015 edition of Rust
    #[serde(rename = "2015")]
    E2015,
}

/// Configuration for the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct HtmlConfig {
    /// The theme directory, if specified.
    pub theme: Option<PathBuf>,
    /// The default theme to use, defaults to 'light'
    pub default_theme: Option<String>,
    /// The theme to use if the browser requests the dark version of the site.
    /// Defaults to 'navy'.
    pub preferred_dark_theme: Option<String>,
    /// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
    pub smart_punctuation: bool,
    /// Support for definition lists.
    pub definition_lists: bool,
    /// Support for admonitions.
    pub admonitions: bool,
    /// Should mathjax be enabled?
    pub mathjax_support: bool,
    /// Additional CSS stylesheets to include in the rendered page's `<head>`.
    pub additional_css: Vec<PathBuf>,
    /// Additional JS scripts to include at the bottom of the rendered page's
    /// `<body>`.
    pub additional_js: Vec<PathBuf>,
    /// Fold settings.
    pub fold: Fold,
    /// Playground settings.
    #[serde(alias = "playpen")]
    pub playground: Playground,
    /// Code settings.
    pub code: Code,
    /// Print settings.
    pub print: Print,
    /// Don't render section labels.
    pub no_section_label: bool,
    /// Search settings. If `None`, the default will be used.
    pub search: Option<Search>,
    /// Git repository url. If `None`, the git button will not be shown.
    pub git_repository_url: Option<String>,
    /// FontAwesome icon class to use for the Git repository link.
    /// Defaults to `fa-github` if `None`.
    pub git_repository_icon: Option<String>,
    /// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output
    pub input_404: Option<String>,
    /// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
    pub site_url: Option<String>,
    /// The DNS subdomain or apex domain at which your book will be hosted. This
    /// string will be written to a file named CNAME in the root of your site,
    /// as required by GitHub Pages (see [*Managing a custom domain for your
    /// GitHub Pages site*][custom domain]).
    ///
    /// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
    pub cname: Option<String>,
    /// Edit url template, when set shows a "Suggest an edit" button for
    /// directly jumping to editing the currently viewed page.
    /// Contains {path} that is replaced with chapter source file path
    pub edit_url_template: Option<String>,
    /// Endpoint of websocket, for livereload usage. Value loaded from .toml
    /// file is ignored, because our code overrides this field with an
    /// internal value (`LIVE_RELOAD_ENDPOINT)
    ///
    /// This config item *should not be edited* by the end user.
    #[doc(hidden)]
    pub live_reload_endpoint: Option<String>,
    /// The mapping from old pages to new pages/URLs to use when generating
    /// redirects.
    pub redirect: HashMap<String, String>,
    /// If this option is turned on, "cache bust" static files by adding
    /// hashes to their file names.
    ///
    /// The default is `true`.
    pub hash_files: bool,
    /// If enabled, the sidebar includes navigation for headers on the current
    /// page. Default is `true`.
    pub sidebar_header_nav: bool,
}

impl Default for HtmlConfig {
    fn default() -> HtmlConfig {
        HtmlConfig {
            theme: None,
            default_theme: None,
            preferred_dark_theme: None,
            smart_punctuation: true,
            definition_lists: true,
            admonitions: true,
            mathjax_support: false,
            additional_css: Vec::new(),
            additional_js: Vec::new(),
            fold: Fold::default(),
            playground: Playground::default(),
            code: Code::default(),
            print: Print::default(),
            no_section_label: false,
            search: None,
            git_repository_url: None,
            git_repository_icon: None,
            input_404: None,
            site_url: None,
            cname: None,
            edit_url_template: None,
            live_reload_endpoint: None,
            redirect: HashMap::new(),
            hash_files: true,
            sidebar_header_nav: true,
        }
    }
}

impl HtmlConfig {
    /// Returns the directory of theme from the provided root directory. If the
    /// directory is not present it will append the default directory of "theme"
    pub fn theme_dir(&self, root: &Path) -> PathBuf {
        match self.theme {
            Some(ref d) => root.join(d),
            None => root.join("theme"),
        }
    }

    /// Returns the name of the file used for HTTP 404 "not found" with the `.html` extension.
    pub fn get_404_output_file(&self) -> String {
        self.input_404
            .as_ref()
            .unwrap_or(&"404.md".to_string())
            .replace(".md", ".html")
    }
}

/// Configuration for how to render the print icon, print.html, and print.css.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct Print {
    /// Whether print support is enabled.
    pub enable: bool,
    /// Insert page breaks between chapters. Default: `true`.
    pub page_break: bool,
}

impl Default for Print {
    fn default() -> Self {
        Self {
            enable: true,
            page_break: true,
        }
    }
}

/// Configuration for how to fold chapters of sidebar.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct Fold {
    /// When off, all folds are open. Default: `false`.
    pub enable: bool,
    /// The higher the more folded regions are open. When level is 0, all folds
    /// are closed.
    /// Default: `0`.
    pub level: u8,
}

/// Configuration for tweaking how the HTML renderer handles the playground.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct Playground {
    /// Should playground snippets be editable? Default: `false`.
    pub editable: bool,
    /// Display the copy button. Default: `true`.
    pub copyable: bool,
    /// Copy JavaScript files for the editor to the output directory?
    /// Default: `true`.
    pub copy_js: bool,
    /// Display line numbers on playground snippets. Default: `false`.
    pub line_numbers: bool,
    /// Display the run button. Default: `true`
    pub runnable: bool,
}

impl Default for Playground {
    fn default() -> Playground {
        Playground {
            editable: false,
            copyable: true,
            copy_js: true,
            line_numbers: false,
            runnable: true,
        }
    }
}

/// Configuration for tweaking how the HTML renderer handles code blocks.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct Code {
    /// A prefix string to hide lines per language (one or more chars).
    pub hidelines: HashMap<String, String>,
}

/// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct Search {
    /// Enable the search feature. Default: `true`.
    pub enable: bool,
    /// Maximum number of visible results. Default: `30`.
    pub limit_results: u32,
    /// The number of words used for a search result teaser. Default: `30`.
    pub teaser_word_count: u32,
    /// Define the logical link between multiple search words.
    /// If true, all search words must appear in each result. Default: `false`.
    pub use_boolean_and: bool,
    /// Boost factor for the search result score if a search word appears in the header.
    /// Default: `2`.
    pub boost_title: u8,
    /// Boost factor for the search result score if a search word appears in the hierarchy.
    /// The hierarchy contains all titles of the parent documents and all parent headings.
    /// Default: `1`.
    pub boost_hierarchy: u8,
    /// Boost factor for the search result score if a search word appears in the text.
    /// Default: `1`.
    pub boost_paragraph: u8,
    /// True if the searchword `micro` should match `microwave`. Default: `true`.
    pub expand: bool,
    /// Documents are split into smaller parts, separated by headings. This defines, until which
    /// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
    pub heading_split_level: u8,
    /// Copy JavaScript files for the search functionality to the output directory?
    /// Default: `true`.
    pub copy_js: bool,
    /// Specifies search settings for the given path.
    ///
    /// The path can be for a specific chapter, or a directory. This will
    /// merge recursively, with more specific paths taking precedence.
    pub chapter: HashMap<String, SearchChapterSettings>,
}

impl Default for Search {
    fn default() -> Search {
        // Please update the documentation of `Search` when changing values!
        Search {
            enable: true,
            limit_results: 30,
            teaser_word_count: 30,
            use_boolean_and: false,
            boost_title: 2,
            boost_hierarchy: 1,
            boost_paragraph: 1,
            expand: true,
            heading_split_level: 3,
            copy_js: true,
            chapter: HashMap::new(),
        }
    }
}

/// Search options for chapters (or paths).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct SearchChapterSettings {
    /// Whether or not indexing is enabled, default `true`.
    pub enable: Option<bool>,
}

/// Allows you to "update" any arbitrary field in a struct by round-tripping via
/// a `toml::Value`.
///
/// This is definitely not the most performant way to do things, which means you
/// should probably keep it away from tight loops...
trait Updateable<'de>: Serialize + Deserialize<'de> {
    fn update_value<S: Serialize>(&mut self, key: &str, value: S) -> Result<()> {
        let mut raw = Value::try_from(&self).expect("unreachable");
        let value = Value::try_from(value)?;
        raw.insert(key, value);
        let updated = raw.try_into()?;
        *self = updated;
        Ok(())
    }
}

impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}

#[cfg(test)]
mod tests {
    use super::*;

    const COMPLEX_CONFIG: &str = r#"
        [book]
        title = "Some Book"
        authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
        description = "A completely useless book"
        src = "source"
        language = "ja"

        [build]
        build-dir = "outputs"
        create-missing = false
        use-default-preprocessors = true

        [output.html]
        theme = "./themedir"
        default-theme = "rust"
        smart-punctuation = true
        additional-css = ["./foo/bar/baz.css"]
        git-repository-url = "https://foo.com/"
        git-repository-icon = "fa-code-fork"

        [output.html.playground]
        editable = true

        [output.html.redirect]
        "index.html" = "overview.html"
        "nexted/page.md" = "https://rust-lang.org/"

        [preprocessor.first]

        [preprocessor.second]
        "#;

    #[test]
    fn load_a_complex_config_file() {
        let src = COMPLEX_CONFIG;

        let book_should_be = BookConfig {
            title: Some(String::from("Some Book")),
            authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
            description: Some(String::from("A completely useless book")),
            src: PathBuf::from("source"),
            language: Some(String::from("ja")),
            text_direction: None,
        };
        let build_should_be = BuildConfig {
            build_dir: PathBuf::from("outputs"),
            create_missing: false,
            use_default_preprocessors: true,
            extra_watch_dirs: Vec::new(),
        };
        let rust_should_be = RustConfig { edition: None };
        let playground_should_be = Playground {
            editable: true,
            copyable: true,
            copy_js: true,
            line_numbers: false,
            runnable: true,
        };
        let html_should_be = HtmlConfig {
            smart_punctuation: true,
            additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
            theme: Some(PathBuf::from("./themedir")),
            default_theme: Some(String::from("rust")),
            playground: playground_should_be,
            git_repository_url: Some(String::from("https://foo.com/")),
            git_repository_icon: Some(String::from("fa-code-fork")),
            redirect: vec![
                (String::from("index.html"), String::from("overview.html")),
                (
                    String::from("nexted/page.md"),
                    String::from("https://rust-lang.org/"),
                ),
            ]
            .into_iter()
            .collect(),
            ..Default::default()
        };

        let got = Config::from_str(src).unwrap();

        assert_eq!(got.book, book_should_be);
        assert_eq!(got.build, build_should_be);
        assert_eq!(got.rust, rust_should_be);
        assert_eq!(got.html_config().unwrap(), html_should_be);
    }

    #[test]
    fn disable_runnable() {
        let src = r#"
        [book]
        title = "Some Book"
        description = "book book book"
        authors = ["Shogo Takata"]

        [output.html.playground]
        runnable = false
        "#;

        let got = Config::from_str(src).unwrap();
        assert!(!got.html_config().unwrap().playground.runnable);
    }

    #[test]
    fn edition_2015() {
        let src = r#"
        [book]
        title = "mdBook Documentation"
        description = "Create book from markdown files. Like Gitbook but implemented in Rust"
        authors = ["Mathieu David"]
        src = "./source"
        [rust]
        edition = "2015"
        "#;

        let book_should_be = BookConfig {
            title: Some(String::from("mdBook Documentation")),
            description: Some(String::from(
                "Create book from markdown files. Like Gitbook but implemented in Rust",
            )),
            authors: vec![String::from("Mathieu David")],
            src: PathBuf::from("./source"),
            ..Default::default()
        };

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.book, book_should_be);

        let rust_should_be = RustConfig {
            edition: Some(RustEdition::E2015),
        };
        let got = Config::from_str(src).unwrap();
        assert_eq!(got.rust, rust_should_be);
    }

    #[test]
    fn edition_2018() {
        let src = r#"
        [book]
        title = "mdBook Documentation"
        description = "Create book from markdown files. Like Gitbook but implemented in Rust"
        authors = ["Mathieu David"]
        src = "./source"
        [rust]
        edition = "2018"
        "#;

        let rust_should_be = RustConfig {
            edition: Some(RustEdition::E2018),
        };

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.rust, rust_should_be);
    }

    #[test]
    fn edition_2021() {
        let src = r#"
        [book]
        title = "mdBook Documentation"
        description = "Create book from markdown files. Like Gitbook but implemented in Rust"
        authors = ["Mathieu David"]
        src = "./source"
        [rust]
        edition = "2021"
        "#;

        let rust_should_be = RustConfig {
            edition: Some(RustEdition::E2021),
        };

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.rust, rust_should_be);
    }

    #[test]
    fn load_arbitrary_output_type() {
        #[derive(Debug, Deserialize, PartialEq)]
        struct RandomOutput {
            foo: u32,
            bar: String,
            baz: Vec<bool>,
        }

        let src = r#"
        [output.random]
        foo = 5
        bar = "Hello World"
        baz = [true, true, false]
        "#;

        let should_be = RandomOutput {
            foo: 5,
            bar: String::from("Hello World"),
            baz: vec![true, true, false],
        };

        let cfg = Config::from_str(src).unwrap();
        let got: RandomOutput = cfg.get("output.random").unwrap().unwrap();

        assert_eq!(got, should_be);

        let got_baz: Vec<bool> = cfg.get("output.random.baz").unwrap().unwrap();
        let baz_should_be = vec![true, true, false];

        assert_eq!(got_baz, baz_should_be);
    }

    #[test]
    fn set_special_tables() {
        let mut cfg = Config::default();
        assert_eq!(cfg.book.title, None);
        cfg.set("book.title", "my title").unwrap();
        assert_eq!(cfg.book.title, Some("my title".to_string()));

        assert_eq!(&cfg.build.build_dir, Path::new("book"));
        cfg.set("build.build-dir", "some-directory").unwrap();
        assert_eq!(&cfg.build.build_dir, Path::new("some-directory"));

        assert_eq!(cfg.rust.edition, None);
        cfg.set("rust.edition", "2024").unwrap();
        assert_eq!(cfg.rust.edition, Some(RustEdition::E2024));

        cfg.set("output.foo.value", "123").unwrap();
        let got: String = cfg.get("output.foo.value").unwrap().unwrap();
        assert_eq!(got, "123");

        cfg.set("preprocessor.bar.value", "456").unwrap();
        let got: String = cfg.get("preprocessor.bar.value").unwrap().unwrap();
        assert_eq!(got, "456");
    }

    #[test]
    fn set_invalid_keys() {
        let mut cfg = Config::default();
        let err = cfg.set("foo", "test").unwrap_err();
        assert!(err.to_string().contains("invalid key `foo`"));
    }

    #[test]
    fn parse_env_vars() {
        let inputs = vec![
            ("FOO", None),
            ("MDBOOK_foo", Some("foo")),
            ("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
            ("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
        ];

        for (src, should_be) in inputs {
            let got = parse_env(src);
            let should_be = should_be.map(ToString::to_string);

            assert_eq!(got, should_be);
        }
    }

    #[test]
    fn file_404_default() {
        let src = r#"
        [output.html]
        "#;

        let got = Config::from_str(src).unwrap();
        let html_config = got.html_config().unwrap();
        assert_eq!(html_config.input_404, None);
        assert_eq!(html_config.get_404_output_file(), "404.html");
    }

    #[test]
    fn file_404_custom() {
        let src = r#"
        [output.html]
        input-404= "missing.md"
        "#;

        let got = Config::from_str(src).unwrap();
        let html_config = got.html_config().unwrap();
        assert_eq!(html_config.input_404, Some("missing.md".to_string()));
        assert_eq!(html_config.get_404_output_file(), "missing.html");
    }

    #[test]
    fn text_direction_ltr() {
        let src = r#"
        [book]
        text-direction = "ltr"
        "#;

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
    }

    #[test]
    fn text_direction_rtl() {
        let src = r#"
        [book]
        text-direction = "rtl"
        "#;

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
    }

    #[test]
    fn text_direction_none() {
        let src = r#"
        [book]
        "#;

        let got = Config::from_str(src).unwrap();
        assert_eq!(got.book.text_direction, None);
    }

    #[test]
    fn test_text_direction() {
        let mut cfg = BookConfig::default();

        // test deriving the text direction from language codes
        cfg.language = Some("ar".into());
        assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);

        cfg.language = Some("he".into());
        assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);

        cfg.language = Some("en".into());
        assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);

        cfg.language = Some("ja".into());
        assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);

        // test forced direction
        cfg.language = Some("ar".into());
        cfg.text_direction = Some(TextDirection::LeftToRight);
        assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);

        cfg.language = Some("ar".into());
        cfg.text_direction = Some(TextDirection::RightToLeft);
        assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);

        cfg.language = Some("en".into());
        cfg.text_direction = Some(TextDirection::LeftToRight);
        assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);

        cfg.language = Some("en".into());
        cfg.text_direction = Some(TextDirection::RightToLeft);
        assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
    }

    #[test]
    #[should_panic(expected = "Invalid configuration file")]
    fn invalid_language_type_error() {
        let src = r#"
        [book]
        title = "mdBook Documentation"
        language = ["en", "pt-br"]
        description = "Create book from markdown files. Like Gitbook but implemented in Rust"
        authors = ["Mathieu David"]
        src = "./source"
        "#;

        Config::from_str(src).unwrap();
    }

    #[test]
    #[should_panic(expected = "Invalid configuration file")]
    fn invalid_title_type() {
        let src = r#"
        [book]
        title = 20
        language = "en"
        description = "Create book from markdown files. Like Gitbook but implemented in Rust"
        authors = ["Mathieu David"]
        src = "./source"
        "#;

        Config::from_str(src).unwrap();
    }

    #[test]
    #[should_panic(expected = "Invalid configuration file")]
    fn invalid_build_dir_type() {
        let src = r#"
        [build]
        build-dir = 99
        create-missing = false
        "#;

        Config::from_str(src).unwrap();
    }

    #[test]
    #[should_panic(expected = "Invalid configuration file")]
    fn invalid_rust_edition() {
        let src = r#"
        [rust]
        edition = "1999"
        "#;

        Config::from_str(src).unwrap();
    }

    #[test]
    #[should_panic(
        expected = "unknown variant `1999`, expected one of `2024`, `2021`, `2018`, `2015`\n"
    )]
    fn invalid_rust_edition_expected() {
        let src = r#"
        [rust]
        edition = "1999"
        "#;

        Config::from_str(src).unwrap();
    }

    #[test]
    fn print_config() {
        let src = r#"
        [output.html.print]
        enable = false
        "#;
        let got = Config::from_str(src).unwrap();
        let html_config = got.html_config().unwrap();
        assert!(!html_config.print.enable);
        assert!(html_config.print.page_break);
        let src = r#"
        [output.html.print]
        page-break = false
        "#;
        let got = Config::from_str(src).unwrap();
        let html_config = got.html_config().unwrap();
        assert!(html_config.print.enable);
        assert!(!html_config.print.page_break);
    }

    #[test]
    fn test_json_direction() {
        use serde_json::json;
        assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
        assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
    }

    #[test]
    fn get_deserialize_error() {
        let src = r#"
        [preprocessor.foo]
        x = 123
        "#;
        let cfg = Config::from_str(src).unwrap();
        let err = cfg.get::<String>("preprocessor.foo.x").unwrap_err();
        assert_eq!(
            err.to_string(),
            "Failed to deserialize `preprocessor.foo.x`"
        );
    }

    #[test]
    fn contains_key() {
        let src = r#"
        [preprocessor.foo]
        x = 123
        [output.foo.sub]
        y = 'x'
        "#;
        let cfg = Config::from_str(src).unwrap();
        assert!(cfg.contains_key("preprocessor.foo"));
        assert!(cfg.contains_key("preprocessor.foo.x"));
        assert!(!cfg.contains_key("preprocessor.bar"));
        assert!(!cfg.contains_key("preprocessor.foo.y"));
        assert!(cfg.contains_key("output.foo"));
        assert!(cfg.contains_key("output.foo.sub"));
        assert!(cfg.contains_key("output.foo.sub.y"));
        assert!(!cfg.contains_key("output.bar"));
        assert!(!cfg.contains_key("output.foo.sub.z"));
    }
}
