use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};

use crate::ser::TranslatedContent;
use crate::sorting::{sort_pages, sort_sections};
use crate::taxonomies::{Taxonomy, TaxonomyFound};
use crate::{Page, Section, SortBy};
use ahash::{AHashMap, AHashSet};
use config::{Config, TaxonomyConfig};
use utils::slugs::slugify_paths;

macro_rules! set {
    ($($key:expr,)+) => (set!($($key),+));

    ( $($key:expr),* ) => {
        {
            let mut _set = AHashSet::new();
            $(
                _set.insert($key);
            )*
            _set
        }
    };
}

#[derive(Debug, Clone)]
pub enum PathCollision<'a> {
    Content { path: &'a str, files: &'a AHashSet<PathBuf> },
    Taxonomy { path: String, taxonomy: &'a str, files: &'a AHashSet<PathBuf> },
    TaxonomyTerm { path: String, taxonomy: &'a str, term: &'a str, files: &'a AHashSet<PathBuf> },
}

impl<'a> Display for PathCollision<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            PathCollision::Content { path, files } => {
                write!(f, "`{path}` from files {files:?}")
            }
            PathCollision::Taxonomy { path, taxonomy, files } => {
                write!(f, "`{path}` with taxonomy {taxonomy} and files {files:?}")
            }
            PathCollision::TaxonomyTerm { path, taxonomy, term, files } => {
                write!(f, "`{path}` with taxonomy {taxonomy} and term {term} and files {files:?}")
            }
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct Library {
    pub pages: AHashMap<PathBuf, Page>,
    pub sections: AHashMap<PathBuf, Section>,
    // aliases -> files, so we can easily check for conflicts
    pub reverse_aliases: AHashMap<String, AHashSet<PathBuf>>,
    pub translations: AHashMap<PathBuf, AHashSet<PathBuf>>,
    pub backlinks: AHashMap<String, AHashSet<PathBuf>>,
    // (internal path, (md path, asset path relative to owner)
    pub colocated_assets: AHashMap<String, (String, String)>,
    // A mapping of {lang -> <slug, {term -> vec<paths>}>>}
    taxonomies_def: AHashMap<String, AHashMap<String, AHashMap<String, Vec<PathBuf>>>>,
    // All the taxonomies from config.toml in their slugified version
    // So we don't need to pass the Config when adding a page to know how to slugify and we only
    // slugify once
    taxo_name_to_slug: AHashMap<String, String>,
}

impl Library {
    pub fn new(config: &Config) -> Self {
        let mut lib = Self::default();

        for (lang, options) in &config.languages {
            let mut taxas = AHashMap::new();
            for tax_def in &options.taxonomies {
                taxas.insert(tax_def.slug.clone(), AHashMap::new());
                lib.taxo_name_to_slug.insert(tax_def.name.clone(), tax_def.slug.clone());
            }
            lib.taxonomies_def.insert(lang.to_string(), taxas);
        }
        lib
    }

    fn insert_reverse_aliases(&mut self, file_path: &Path, entries: Vec<String>) {
        for entry in entries {
            self.reverse_aliases
                .entry(entry)
                .and_modify(|s| {
                    s.insert(file_path.to_path_buf());
                })
                .or_insert_with(|| set! {file_path.to_path_buf()});
        }
    }

    /// This will check every section/page paths + the aliases + taxonomies and their terms
    /// and ensure none of them are colliding.
    pub fn find_path_collisions<'a>(&'a self, config: &'a Config) -> Vec<PathCollision<'a>> {
        let mut collisions = Vec::new();

        for (path, files) in &self.reverse_aliases {
            if files.len() > 1 {
                collisions.push(PathCollision::Content { path: path.as_str(), files });
            }
        }

        for (lang, lang_options) in &config.languages {
            for tax_def in &lang_options.taxonomies {
                if !tax_def.render {
                    continue;
                }

                let taxo_path = config.get_taxonomy_path(lang, tax_def);
                if let Some(files) = self.reverse_aliases.get(&taxo_path) {
                    collisions.push(PathCollision::Taxonomy {
                        path: taxo_path,
                        taxonomy: tax_def.name.as_str(),
                        files,
                    });
                }

                if let Some(lang_taxonomies) = self.taxonomies_def.get(lang)
                    && let Some(terms) = lang_taxonomies.get(&tax_def.slug)
                {
                    for term_name in terms.keys() {
                        let term_slug = slugify_paths(term_name, config.slugify.taxonomies);
                        let term_path = config.get_taxonomy_term_path(lang, tax_def, &term_slug);
                        if let Some(files) = self.reverse_aliases.get(&term_path) {
                            collisions.push(PathCollision::TaxonomyTerm {
                                path: term_path,
                                taxonomy: tax_def.name.as_str(),
                                term: term_name.as_str(),
                                files,
                            });
                        }
                    }
                }
            }
        }
        collisions
    }

    pub fn insert_page(&mut self, page: Page) {
        let file_path = page.file.path.clone();
        if page.meta.render {
            let mut entries = vec![page.path.clone()];
            entries.extend(page.meta.aliases.to_vec());
            self.insert_reverse_aliases(&file_path, entries);

            for (taxa_name, terms) in &page.meta.taxonomies {
                for term in terms {
                    // Safe unwraps as we create all lang/taxa and we validated that they are correct
                    // before getting there
                    let taxa_def = self
                        .taxonomies_def
                        .get_mut(&page.lang)
                        .expect("lang not found")
                        .get_mut(&self.taxo_name_to_slug[taxa_name])
                        .expect("taxa not found");

                    taxa_def.entry(term.to_string()).or_default().push(page.file.path.clone());
                }
            }
        }

        self.colocated_assets.extend(page.colocated_assets.clone());
        self.pages.insert(file_path, page);
    }

    pub fn insert_section(&mut self, section: Section) {
        let file_path = section.file.path.clone();
        if section.meta.render {
            let mut entries = vec![section.path.clone()];
            entries.extend(section.meta.aliases.to_vec());
            self.insert_reverse_aliases(&file_path, entries);
        }
        self.colocated_assets.extend(section.colocated_assets.clone());
        self.sections.insert(file_path, section);
    }

    /// Fills a map of target -> {content mentioning it}
    /// This can only be called _after_ rendering markdown as we need to have accumulated all
    /// the links first
    pub fn fill_backlinks(&mut self) {
        self.backlinks.clear();

        let mut add_backlink = |target: &str, source: &Path| {
            self.backlinks
                .entry(target.to_owned())
                .and_modify(|s| {
                    s.insert(source.to_path_buf());
                })
                .or_insert(set! {source.to_path_buf()});
        };

        for (_, page) in &self.pages {
            for (internal_link, _) in &page.internal_links {
                add_backlink(internal_link, &page.file.path);
            }
        }
        for (_, section) in &self.sections {
            for (internal_link, _) in &section.internal_links {
                add_backlink(internal_link, &section.file.path);
            }
        }
    }

    /// This is called _before_ rendering the markdown the pages/sections
    pub fn find_taxonomies(&self, config: &Config) -> Vec<Taxonomy> {
        let mut taxonomies = Vec::new();

        let taxo_configs: AHashMap<(&str, &str), &TaxonomyConfig> = config
            .languages
            .iter()
            .flat_map(|(lang, lang_config)| {
                lang_config.taxonomies.iter().map(move |t| ((lang.as_str(), t.slug.as_str()), t))
            })
            .collect();

        for (lang, taxonomies_data) in &self.taxonomies_def {
            for (taxa_slug, terms_pages) in taxonomies_data {
                let taxo_config = taxo_configs
                    .get(&(lang.as_str(), taxa_slug.as_str()))
                    .expect("taxo should exist");
                let mut taxo_found = TaxonomyFound::new(taxa_slug.to_string(), lang, taxo_config);
                for (term, page_path) in terms_pages {
                    let pages: Vec<_> =
                        page_path.iter().map(|p| &self.pages[p]).filter(|p| !p.hidden).collect();
                    if !pages.is_empty() {
                        taxo_found.terms.insert(term, pages);
                    }
                }

                taxonomies.push(Taxonomy::new(taxo_found, config));
            }
        }

        taxonomies
    }

    /// Sort all sections pages according to sorting method given
    /// Pages that cannot be sorted are set to the section.ignored_pages instead
    pub fn sort_section_pages(&mut self) {
        let mut updates = AHashMap::new();
        for (path, section) in &self.sections {
            let pages: Vec<_> = section.pages.iter().map(|p| &self.pages[p]).collect();
            let (sorted_pages, cannot_be_sorted_pages) = match section.meta.sort_by {
                SortBy::None => continue,
                _ => sort_pages(&pages, section.meta.sort_by),
            };

            updates
                .insert(path.clone(), (sorted_pages, cannot_be_sorted_pages, section.meta.sort_by));
        }

        for (path, (sorted, unsortable, _)) in updates {
            if !self.sections[&path].meta.transparent {
                // Fill siblings
                for (i, page_path) in sorted.iter().enumerate() {
                    let p = self.pages.get_mut(page_path).unwrap();
                    if i > 0 {
                        // lighter / later / title_prev
                        p.lower = Some(sorted[i - 1].clone());
                    }

                    if i < sorted.len() - 1 {
                        // heavier / earlier / title_next
                        p.higher = Some(sorted[i + 1].clone());
                    }
                }
            }

            if let Some(s) = self.sections.get_mut(&path) {
                s.pages = sorted;
                s.ignored_pages = unsortable;
            }
        }
    }

    /// Sort all sections subsections according to sorting method given
    /// Sections that cannot be sorted are set to the section.ignored_subsections instead
    pub fn sort_section_subsections(&mut self) {
        let mut updates = AHashMap::new();
        for (path, section) in &self.sections {
            let sections: Vec<_> = section
                .subsections
                .iter()
                .map(|p| &self.sections[p])
                .filter(|s| !s.hidden)
                .collect();
            let (sorted_sections, cannot_be_sorted_sections) =
                sort_sections(&sections, section.meta.sort_by);

            updates.insert(
                path.clone(),
                (sorted_sections, cannot_be_sorted_sections, section.meta.sort_by),
            );
        }

        for (path, (sorted, unsortable, _)) in updates {
            if !self.sections[&path].meta.transparent {
                // Fill siblings
                for (i, section_path) in sorted.iter().enumerate() {
                    let p = self.sections.get_mut(section_path).unwrap();
                    if i > 0 {
                        // lighter / later / title_prev
                        p.lower = Some(sorted[i - 1].clone());
                    }

                    if i < sorted.len() - 1 {
                        // heavier / earlier / title_next
                        p.higher = Some(sorted[i + 1].clone());
                    }
                }
            }

            if let Some(s) = self.sections.get_mut(&path) {
                s.subsections = sorted;
                s.ignored_subsections = unsortable;
            }
        }
    }

    /// Find out the direct subsections of each subsection if there are some
    /// as well as the pages for each section
    pub fn populate_sections(&mut self, config: &Config, content_path: &Path) {
        let mut add_translation = |entry: &Path, path: &Path| {
            if config.is_multilingual() {
                self.translations
                    .entry(entry.to_path_buf())
                    .and_modify(|trans| {
                        trans.insert(path.to_path_buf());
                    })
                    .or_insert(set! {path.to_path_buf()});
            }
        };

        let mut ancestors = AHashMap::new();
        let mut subsections = AHashMap::new();
        let mut hidden_by_relative = AHashMap::new();

        // We iterate over the sections twice
        // The first time to build up the list of ancestors for each section
        for (path, section) in &self.sections {
            hidden_by_relative.insert(section.file.relative.clone(), section.meta.hidden);
            if let Some(ref grand_parent) = section.file.grand_parent {
                subsections
                    // Using the original filename to work for multi-lingual sections
                    .entry(grand_parent.join(&section.file.filename))
                    .or_insert_with(Vec::new)
                    .push(section.file.path.clone());
            }

            add_translation(&section.file.canonical, path);

            // Root sections have no ancestors
            if section.is_index() {
                ancestors.insert(section.file.path.clone(), vec![]);
                continue;
            }

            // Index section is the first ancestor of every single section
            let mut cur_path = content_path.to_path_buf();
            let mut parents = vec![section.file.filename.clone()];
            for component in &section.file.components {
                cur_path = cur_path.join(component);
                // Skip itself
                if cur_path == section.file.parent {
                    continue;
                }

                let index_path = cur_path.join(&section.file.filename);
                if let Some(s) = self.sections.get(&index_path) {
                    parents.push(s.file.relative.clone());
                }
            }
            ancestors.insert(section.file.path.clone(), parents);
        }

        for (path, section) in self.sections.iter_mut() {
            section.subsections.clear();
            section.ignored_subsections.clear();
            section.pages.clear();
            section.hidden_pages.clear();
            section.ignored_pages.clear();
            section.ancestors.clear();

            if let Some(children) = subsections.get(path) {
                section.subsections = children.clone();
            }
            if let Some(parents) = ancestors.get(path) {
                section.ancestors = parents.clone();
            }

            // We look at the closest ancestor that has it set to true
            let mut hidden = section.meta.hidden;
            if hidden.is_none() {
                for ancestor in section.ancestors.iter().rev() {
                    if let Some(val) = hidden_by_relative[ancestor] {
                        hidden = Some(val);
                        break;
                    }
                }
            }
            section.hidden = hidden.unwrap_or(false);
        }

        // We pre-build the index filename for each language
        let mut index_filename_by_lang = AHashMap::with_capacity(config.languages.len());
        for code in config.languages.keys() {
            if code == &config.default_language {
                index_filename_by_lang.insert(code, "_index.md".to_owned());
            } else {
                index_filename_by_lang.insert(code, format!("_index.{}.md", code));
            }
        }

        // Then once we took care of the sections, we find the pages of each section
        for (path, page) in self.pages.iter_mut() {
            if !page.meta.render {
                continue;
            }
            let parent_filename = &index_filename_by_lang[&page.lang];
            add_translation(&page.file.canonical, path);
            let mut parent_section_path = page.file.parent.join(parent_filename);

            // We've resolved the sections visibility before so we will just take the parent one
            // if hidden is not explicitely set
            page.hidden = page.meta.hidden.unwrap_or_else(|| {
                self.sections.get(&parent_section_path).map(|s| s.hidden).unwrap_or(false)
            });

            while let Some(parent_section) = self.sections.get_mut(&parent_section_path) {
                let is_transparent = parent_section.meta.transparent;
                if !page.hidden {
                    parent_section.pages.push(path.clone());
                } else {
                    // We track hidden pages as well to render them
                    parent_section.hidden_pages.push(path.clone());
                }
                page.ancestors = ancestors.get(&parent_section_path).cloned().unwrap_or_default();
                // Don't forget to push the actual parent
                page.ancestors.push(parent_section.file.relative.clone());

                // Find the page template if one of a parent has page_template set
                // Stops after the first one found, keep in mind page.ancestors
                // is [index, ..., parent] so we need to reverse it first
                if page.meta.template.is_none() {
                    for ancestor in page.ancestors.iter().rev() {
                        let s = self.sections.get(&content_path.join(ancestor)).unwrap();
                        if let Some(ref tpl) = s.meta.page_template {
                            page.meta.template = Some(tpl.clone());
                            break;
                        }
                    }
                }

                if !is_transparent {
                    break;
                }

                // We've added `_index(.{LANG})?.md` so if we are here so we need to go up twice
                match parent_section_path.clone().parent().unwrap().parent() {
                    Some(parent) => parent_section_path = parent.join(parent_filename),
                    None => break,
                }
            }
        }

        // And once we have all the pages assigned to their section, we sort them
        self.sort_section_pages();
        self.sort_section_subsections();
    }

    /// Find all the orphan pages: pages that are in a folder without an `_index.md`
    pub fn get_all_orphan_pages(&self) -> Vec<&Page> {
        self.pages.iter().filter(|(_, p)| p.ancestors.is_empty()).map(|(_, p)| p).collect()
    }

    /// Find all the translated content for a given canonical path.
    /// The translated content can be either for a section or a page
    pub fn find_translations(&self, canonical_path: &Path) -> Vec<TranslatedContent<'_>> {
        let mut translations = vec![];

        if let Some(paths) = self.translations.get(canonical_path) {
            for path in paths {
                let (lang, permalink, title, path) = {
                    if let Some(s) = self.sections.get(path) {
                        (&s.lang, &s.permalink, &s.meta.title, &s.file.path)
                    } else {
                        let s = &self.pages[path];
                        (&s.lang, &s.permalink, &s.meta.title, &s.file.path)
                    }
                };
                translations.push(TranslatedContent { lang, permalink, title, path });
            }
        }

        translations
    }

    pub fn find_pages_by_path(&self, paths: &[PathBuf]) -> Vec<&Page> {
        paths.iter().map(|p| &self.pages[p]).collect()
    }

    pub fn find_sections_by_path(&self, paths: &[PathBuf]) -> Vec<&Section> {
        paths.iter().map(|p| &self.sections[p]).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{FileInfo, SortBy};
    use config::{LanguageOptions, TaxonomyConfig};
    use std::collections::HashMap;
    use utils::slugs::SlugifyStrategy;

    #[test]
    fn can_find_collisions_with_paths() {
        let config = Config::default_for_test();
        let mut library = Library::default();
        let mut section = Section { path: "hello".to_owned(), ..Default::default() };
        section.file.path = PathBuf::from("hello.md");
        library.insert_section(section.clone());
        let mut section2 = Section { path: "hello".to_owned(), ..Default::default() };
        section2.file.path = PathBuf::from("bonjour.md");
        library.insert_section(section2.clone());

        let collisions = library.find_path_collisions(&config);
        assert_eq!(collisions.len(), 1);
        if let PathCollision::Content { path, files } = collisions[0] {
            assert_eq!(path, "hello");
            assert!(files.contains(&section.file.path));
            assert!(files.contains(&section2.file.path));
        } else {
            assert!(false);
        }
    }

    #[test]
    fn can_find_collisions_with_aliases() {
        let config = Config::default_for_test();
        let mut library = Library::default();
        let mut section = Section { path: "hello".to_owned(), ..Default::default() };
        section.file.path = PathBuf::from("hello.md");
        library.insert_section(section.clone());
        let mut section2 = Section { path: "world".to_owned(), ..Default::default() };
        section2.file.path = PathBuf::from("bonjour.md");
        section2.meta.aliases = vec!["hello".to_owned(), "hola".to_owned()];
        library.insert_section(section2.clone());
        // Sections with render=false do not collide with anything
        // https://github.com/getzola/zola/issues/1656
        let mut section3 = Section { path: "world2".to_owned(), ..Default::default() };
        section3.meta.render = false;
        section3.file.path = PathBuf::from("bonjour2.md");
        section3.meta.aliases = vec!["hola".to_owned()];
        library.insert_section(section3);

        let collisions = library.find_path_collisions(&config);
        assert_eq!(collisions.len(), 1);
        if let PathCollision::Content { path, files } = collisions[0] {
            assert_eq!(path, "hello");
            assert!(files.contains(&section.file.path));
            assert!(files.contains(&section2.file.path));
        } else {
            assert!(false);
        }
    }

    // https://github.com/getzola/zola/issues/3096
    #[test]
    fn can_find_collisions_with_taxonomy() {
        let mut config = Config::default_for_test();
        config.languages.get_mut("en").unwrap().taxonomies =
            vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }];
        config.slugify_taxonomies();

        let mut library = Library::new(&config);

        // Create a section that conflicts with the "tags" taxonomy path
        let mut section = Section { path: "/tags/".to_owned(), ..Default::default() };
        section.file.path = PathBuf::from("content/tags/_index.md");
        library.insert_section(section.clone());

        let collisions = library.find_path_collisions(&config);
        assert_eq!(collisions.len(), 1);
        if let PathCollision::Taxonomy { path, taxonomy, files } = &collisions[0] {
            assert_eq!(path, "/tags/");
            assert_eq!(*taxonomy, "tags");
            assert!(files.contains(&section.file.path));
        } else {
            assert!(false);
        }
    }

    // https://github.com/getzola/zola/issues/3096
    #[test]
    fn can_find_collisions_with_taxonomy_term() {
        let mut config = Config::default_for_test();
        config.languages.get_mut("en").unwrap().taxonomies =
            vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }];
        config.slugify_taxonomies();

        let mut library = Library::new(&config);

        // Create a page with a "rust" tag to populate the taxonomy terms
        let page = create_page_w_taxa("a.md", "en", vec![("tags", vec!["rust"])]);
        library.insert_page(page);

        // Create a section that conflicts with the "tags/rust" term path
        let mut section = Section { path: "/tags/rust/".to_owned(), ..Default::default() };
        section.file.path = PathBuf::from("content/tags/rust/_index.md");
        library.insert_section(section.clone());

        let collisions = library.find_path_collisions(&config);
        assert_eq!(collisions.len(), 1);

        if let PathCollision::TaxonomyTerm { path, taxonomy, term, files } = &collisions[0] {
            assert_eq!(path, "/tags/rust/");
            assert_eq!(*taxonomy, "tags");
            assert_eq!(*term, "rust");
            assert!(files.contains(&section.file.path));
        } else {
            assert!(false);
        }
    }

    #[derive(Debug, Clone)]
    enum PageSort {
        None,
        Date(&'static str),
        Title(&'static str),
        Weight(usize),
    }

    fn create_page(file_path: &str, lang: &str, page_sort: PageSort, hidden: Option<bool>) -> Page {
        let mut page = Page::default();
        page.lang = lang.to_owned();
        page.file = FileInfo::new_page(Path::new(file_path), &PathBuf::new());
        page.meta.hidden = hidden;
        match page_sort {
            PageSort::None => (),
            PageSort::Date(date) => {
                page.meta.date = Some(date.to_owned());
                page.meta.date_to_datetime();
            }
            PageSort::Title(title) => {
                page.meta.title = Some(title.to_owned());
            }
            PageSort::Weight(w) => {
                page.meta.weight = Some(w);
            }
        }
        page.file.find_language("en", &["fr"]).unwrap();
        page
    }

    fn create_section(
        file_path: &str,
        lang: &str,
        weight: usize,
        transparent: bool,
        sort_by: SortBy,
        hidden: Option<bool>,
    ) -> Section {
        let mut section = Section::default();
        section.lang = lang.to_owned();
        section.file = FileInfo::new_section(Path::new(file_path), &PathBuf::new());
        section.meta.weight = weight;
        section.meta.transparent = transparent;
        section.meta.sort_by = sort_by;
        section.meta.page_template = Some("new_page.html".to_owned());
        section.meta.hidden = hidden;
        section.file.find_language("en", &["fr"]).unwrap();
        section
    }

    #[test]
    fn can_populate_sections() {
        let mut config = Config::default_for_test();
        config.languages.insert("fr".to_owned(), LanguageOptions::default());
        let mut library = Library::default();
        let sections = vec![
            ("content/_index.md", "en", 0, false, SortBy::None, None),
            ("content/_index.fr.md", "fr", 0, false, SortBy::None, None),
            ("content/blog/_index.md", "en", 0, false, SortBy::Date, None),
            ("content/wiki/_index.md", "en", 0, false, SortBy::Weight, None),
            ("content/wiki/_index.fr.md", "fr", 0, false, SortBy::Weight, None),
            ("content/wiki/recipes/_index.md", "en", 1, true, SortBy::Weight, None),
            ("content/wiki/recipes/_index.fr.md", "fr", 1, true, SortBy::Weight, None),
            ("content/wiki/programming/_index.md", "en", 10, true, SortBy::Weight, None),
            ("content/wiki/programming/_index.fr.md", "fr", 10, true, SortBy::Weight, None),
            ("content/novels/_index.md", "en", 10, false, SortBy::Title, None),
            ("content/novels/_index.fr.md", "fr", 10, false, SortBy::Title, None),
            ("content/secret/_index.md", "en", 20, false, SortBy::Weight, Some(true)),
            ("content/secret/inner/_index.md", "en", 1, false, SortBy::Weight, None),
            ("content/secret/visible/_index.md", "en", 2, false, SortBy::Weight, Some(false)),
        ];
        for (p, l, w, t, s, h) in sections.clone() {
            library.insert_section(create_section(p, l, w, t, s, h));
        }

        let pages = vec![
            ("content/about.md", "en", PageSort::None, None),
            ("content/about.fr.md", "en", PageSort::None, None),
            ("content/blog/rust.md", "en", PageSort::Date("2022-01-01"), None),
            ("content/blog/python.md", "en", PageSort::Date("2022-03-03"), None),
            ("content/blog/docker.md", "en", PageSort::Date("2022-02-02"), None),
            ("content/wiki/recipes/chocolate-cake.md", "en", PageSort::Weight(100), None),
            ("content/wiki/recipes/chocolate-cake.fr.md", "fr", PageSort::Weight(100), None),
            ("content/wiki/recipes/rendang.md", "en", PageSort::Weight(5), None),
            ("content/wiki/recipes/rendang.fr.md", "fr", PageSort::Weight(5), None),
            ("content/wiki/programming/rust.md", "en", PageSort::Weight(1), None),
            ("content/wiki/programming/rust.fr.md", "fr", PageSort::Weight(1), None),
            ("content/wiki/programming/zola.md", "en", PageSort::Weight(10), None),
            ("content/wiki/programming/python.md", "en", PageSort::None, None),
            (
                "content/novels/the-colour-of-magic.md",
                "en",
                PageSort::Title("The Colour of Magic"),
                None,
            ),
            (
                "content/novels/the-colour-of-magic.fr.md",
                "en",
                PageSort::Title("La Huitième Couleur"),
                None,
            ),
            ("content/novels/reaper.md", "en", PageSort::Title("Reaper"), None),
            ("content/novels/reaper.fr.md", "fr", PageSort::Title("Reaper (fr)"), None),
            ("content/random/hello.md", "en", PageSort::None, None),
            ("content/blog/surprise.md", "en", PageSort::Date("2021-01-01"), Some(true)),
            ("content/secret/first.md", "en", PageSort::Weight(1), None),
            ("content/secret/unhidden.md", "en", PageSort::Weight(2), Some(false)),
            ("content/secret/inner/deep.md", "en", PageSort::Weight(1), None),
            ("content/secret/visible/shown.md", "en", PageSort::Weight(1), None),
        ];
        for (p, l, s, h) in pages.clone() {
            library.insert_page(create_page(p, l, s, h));
        }
        library.populate_sections(&config, Path::new("content"));
        assert_eq!(library.sections.len(), sections.len());
        assert_eq!(library.pages.len(), pages.len());
        let blog_section = &library.sections[&PathBuf::from("content/blog/_index.md")];
        assert_eq!(blog_section.pages.len(), 3);
        // sorted by date in desc order
        assert_eq!(
            blog_section.pages,
            vec![
                PathBuf::from("content/blog/python.md"),
                PathBuf::from("content/blog/docker.md"),
                PathBuf::from("content/blog/rust.md")
            ]
        );
        assert_eq!(blog_section.ignored_pages.len(), 0);
        assert!(&library.pages[&PathBuf::from("content/blog/python.md")].lower.is_none());
        assert_eq!(
            &library.pages[&PathBuf::from("content/blog/python.md")].higher,
            &Some(PathBuf::from("content/blog/docker.md"))
        );
        assert_eq!(
            library.pages[&PathBuf::from("content/blog/python.md")].meta.template,
            Some("new_page.html".to_owned())
        );

        let wiki = &library.sections[&PathBuf::from("content/wiki/_index.md")];
        assert_eq!(wiki.pages.len(), 4);
        // sorted by weight, in asc order
        assert_eq!(
            wiki.pages,
            vec![
                PathBuf::from("content/wiki/programming/rust.md"),
                PathBuf::from("content/wiki/recipes/rendang.md"),
                PathBuf::from("content/wiki/programming/zola.md"),
                PathBuf::from("content/wiki/recipes/chocolate-cake.md"),
            ]
        );
        assert_eq!(wiki.ignored_pages.len(), 1);
        assert_eq!(wiki.ignored_pages, vec![PathBuf::from("content/wiki/programming/python.md")]);
        assert_eq!(
            &library.pages[&PathBuf::from("content/wiki/recipes/rendang.md")].lower,
            &Some(PathBuf::from("content/wiki/programming/rust.md"))
        );
        assert_eq!(
            &library.pages[&PathBuf::from("content/wiki/recipes/rendang.md")].higher,
            &Some(PathBuf::from("content/wiki/programming/zola.md"))
        );
        assert_eq!(
            wiki.subsections,
            vec![
                PathBuf::from("content/wiki/recipes/_index.md"),
                PathBuf::from("content/wiki/programming/_index.md")
            ]
        );
        assert_eq!(wiki.ancestors, vec!["_index.md".to_owned()]);
        assert_eq!(
            library.sections[&PathBuf::from("content/wiki/recipes/_index.md")].ancestors,
            vec!["_index.md".to_owned(), "wiki/_index.md".to_owned()]
        );

        // also works for other languages
        let french_wiki = &library.sections[&PathBuf::from("content/wiki/_index.fr.md")];
        assert_eq!(french_wiki.pages.len(), 3);
        // sorted by weight, in asc order
        assert_eq!(
            french_wiki.pages,
            vec![
                PathBuf::from("content/wiki/programming/rust.fr.md"),
                PathBuf::from("content/wiki/recipes/rendang.fr.md"),
                PathBuf::from("content/wiki/recipes/chocolate-cake.fr.md"),
            ]
        );
        assert_eq!(french_wiki.ignored_pages.len(), 0);
        assert!(
            &library.pages[&PathBuf::from("content/wiki/recipes/chocolate-cake.fr.md")]
                .higher
                .is_none()
        );
        assert_eq!(
            &library.pages[&PathBuf::from("content/wiki/recipes/chocolate-cake.fr.md")].lower,
            &Some(PathBuf::from("content/wiki/recipes/rendang.fr.md"))
        );

        let orphans = library.get_all_orphan_pages();
        assert_eq!(orphans.len(), 1);
        assert_eq!(orphans[0].file.path, PathBuf::from("content/random/hello.md"));

        // And translations should be filled in
        let translations = library.find_translations(&PathBuf::from("content/novels/reaper"));
        assert_eq!(translations.len(), 2);
        assert!(translations[0].title.is_some());
        assert!(translations[1].title.is_some());

        // Visibility should be correct
        let secret = &library.sections[&PathBuf::from("content/secret/_index.md")];
        assert!(secret.hidden);
        assert_eq!(secret.pages, vec![PathBuf::from("content/secret/unhidden.md")]);
        // Hidden pages are kept out of `pages` but tracked so they can still be rendered
        assert_eq!(secret.hidden_pages, vec![PathBuf::from("content/secret/first.md")]);
        assert_eq!(
            library.sections[&PathBuf::from("content/secret/inner/_index.md")].hidden_pages,
            vec![PathBuf::from("content/secret/inner/deep.md")]
        );
        assert_eq!(
            library.sections[&PathBuf::from("content/blog/_index.md")].hidden_pages,
            vec![PathBuf::from("content/blog/surprise.md")]
        );
        assert_eq!(secret.subsections, vec![PathBuf::from("content/secret/visible/_index.md")]);
        assert!(library.sections[&PathBuf::from("content/secret/inner/_index.md")].hidden);
        let visible = &library.sections[&PathBuf::from("content/secret/visible/_index.md")];
        assert!(!visible.hidden);
        assert_eq!(visible.pages, vec![PathBuf::from("content/secret/visible/shown.md")]);
        assert!(library.pages[&PathBuf::from("content/secret/first.md")].hidden);
        assert!(!library.pages[&PathBuf::from("content/secret/unhidden.md")].hidden);
        assert!(library.pages[&PathBuf::from("content/secret/inner/deep.md")].hidden);
        assert!(!library.pages[&PathBuf::from("content/secret/visible/shown.md")].hidden);
        assert!(library.pages[&PathBuf::from("content/blog/surprise.md")].hidden);
    }

    macro_rules! taxonomies {
        ($config:expr, [$($page:expr),+]) => {{
            let mut library = Library::new(&$config);
            $(
                library.insert_page($page);
            )+
            library.find_taxonomies(&$config)
        }};
    }

    fn create_page_w_taxa(path: &str, lang: &str, taxo: Vec<(&str, Vec<&str>)>) -> Page {
        let mut page = Page::default();
        page.file.path = PathBuf::from(path);
        page.lang = lang.to_owned();
        let mut taxonomies = HashMap::new();
        for (name, terms) in taxo {
            taxonomies.insert(name.to_owned(), terms.iter().map(|t| t.to_string()).collect());
        }
        page.meta.taxonomies = taxonomies;
        page
    }

    #[test]
    fn can_make_taxonomies() {
        let mut config = Config::default_for_test();
        config.languages.get_mut("en").unwrap().taxonomies = vec![
            TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
        ];
        config.slugify_taxonomies();

        let page1 = create_page_w_taxa(
            "a.md",
            "en",
            vec![("tags", vec!["rust", "db"]), ("categories", vec!["tutorials"])],
        );
        let page2 = create_page_w_taxa(
            "b.md",
            "en",
            vec![("tags", vec!["rust", "js"]), ("categories", vec!["others"])],
        );
        let page3 = create_page_w_taxa(
            "c.md",
            "en",
            vec![("tags", vec!["js"]), ("authors", vec!["Vincent Prouillet"])],
        );
        let taxonomies = taxonomies!(config, [page1, page2, page3]);

        let tags = taxonomies.iter().find(|t| t.kind.name == "tags").unwrap();
        assert_eq!(tags.len(), 3);
        assert_eq!(tags.items[0].name, "db");
        assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
        assert_eq!(tags.items[0].pages.len(), 1);
        assert_eq!(tags.items[1].name, "js");
        assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/");
        assert_eq!(tags.items[1].pages.len(), 2);
        assert_eq!(tags.items[2].name, "rust");
        assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/");
        assert_eq!(tags.items[2].pages.len(), 2);

        let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap();
        assert_eq!(categories.items.len(), 2);
        assert_eq!(categories.items[0].name, "others");
        assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/others/");
        assert_eq!(categories.items[0].pages.len(), 1);

        let authors = taxonomies.iter().find(|t| t.kind.name == "authors").unwrap();
        assert_eq!(authors.items.len(), 1);
        assert_eq!(authors.items[0].permalink, "http://a-website.com/authors/vincent-prouillet/");
    }

    #[test]
    fn can_make_multiple_language_taxonomies() {
        let mut config = Config::default_for_test();
        config.slugify.taxonomies = SlugifyStrategy::Safe;
        config.languages.insert("fr".to_owned(), LanguageOptions::default());
        config.languages.get_mut("en").unwrap().taxonomies = vec![
            TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
        ];
        config.languages.get_mut("fr").unwrap().taxonomies = vec![
            TaxonomyConfig { name: "catégories".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
        ];
        config.slugify_taxonomies();

        let page1 = create_page_w_taxa("a.md", "en", vec![("categories", vec!["rust"])]);
        let page2 = create_page_w_taxa("b.md", "en", vec![("tags", vec!["rust"])]);
        let page3 = create_page_w_taxa("c.md", "fr", vec![("catégories", vec!["rust"])]);
        let taxonomies = taxonomies!(config, [page1, page2, page3]);

        let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap();
        assert_eq!(categories.len(), 1);
        assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/rust/");
        let tags = taxonomies.iter().find(|t| t.kind.name == "tags" && t.lang == "en").unwrap();
        assert_eq!(tags.len(), 1);
        assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/rust/");
        let fr_categories = taxonomies.iter().find(|t| t.kind.name == "catégories").unwrap();
        assert_eq!(fr_categories.len(), 1);
        assert_eq!(fr_categories.items[0].permalink, "http://a-website.com/fr/catégories/rust/");
    }

    #[test]
    fn taxonomies_with_unic_are_grouped_with_default_slugify_strategy() {
        let mut config = Config::default_for_test();
        config.languages.get_mut("en").unwrap().taxonomies = vec![
            TaxonomyConfig { name: "test-taxonomy".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "test taxonomy".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "test-taxonomy ".to_string(), ..TaxonomyConfig::default() },
            TaxonomyConfig { name: "Test-Taxonomy ".to_string(), ..TaxonomyConfig::default() },
        ];
        config.slugify_taxonomies();
        let page1 = create_page_w_taxa("a.md", "en", vec![("test-taxonomy", vec!["Ecole"])]);
        let page2 = create_page_w_taxa("b.md", "en", vec![("test taxonomy", vec!["École"])]);
        let page3 = create_page_w_taxa("c.md", "en", vec![("test-taxonomy ", vec!["ecole"])]);
        let page4 = create_page_w_taxa("d.md", "en", vec![("Test-Taxonomy ", vec!["école"])]);
        let taxonomies = taxonomies!(config, [page1, page2, page3, page4]);
        assert_eq!(taxonomies.len(), 1);

        let tax = &taxonomies[0];
        // under the default slugify strategy all of the provided terms should be the same
        assert_eq!(tax.items.len(), 1);
        let term1 = &tax.items[0];
        assert_eq!(term1.name, "Ecole");
        assert_eq!(term1.slug, "ecole");
        assert_eq!(term1.permalink, "http://a-website.com/test-taxonomy/ecole/");
        assert_eq!(term1.pages.len(), 4);
    }

    #[test]
    fn taxonomies_with_unic_are_not_grouped_with_safe_slugify_strategy() {
        let mut config = Config::default_for_test();
        config.slugify.taxonomies = SlugifyStrategy::Safe;
        config.languages.get_mut("en").unwrap().taxonomies =
            vec![TaxonomyConfig { name: "test".to_string(), ..TaxonomyConfig::default() }];
        config.slugify_taxonomies();
        let page1 = create_page_w_taxa("a.md", "en", vec![("test", vec!["Ecole"])]);
        let page2 = create_page_w_taxa("b.md", "en", vec![("test", vec!["École"])]);
        let page3 = create_page_w_taxa("c.md", "en", vec![("test", vec!["ecole"])]);
        let page4 = create_page_w_taxa("d.md", "en", vec![("test", vec!["école"])]);
        let taxonomies = taxonomies!(config, [page1, page2, page3, page4]);
        assert_eq!(taxonomies.len(), 1);
        let tax = &taxonomies[0];
        // under the safe slugify strategy all terms should be distinct
        assert_eq!(tax.items.len(), 4);
    }

    #[test]
    fn can_fill_backlinks() {
        let mut page1 = create_page("page1.md", "en", PageSort::None, None);
        page1.internal_links.push(("page2.md".to_owned(), None));
        let mut page2 = create_page("page2.md", "en", PageSort::None, None);
        page2.internal_links.push(("_index.md".to_owned(), None));
        let mut section1 = create_section("_index.md", "en", 10, false, SortBy::None, None);
        section1.internal_links.push(("page1.md".to_owned(), None));
        section1.internal_links.push(("page2.md".to_owned(), None));
        let mut library = Library::default();
        library.insert_page(page1);
        library.insert_page(page2);
        library.insert_section(section1);
        library.fill_backlinks();

        assert_eq!(library.backlinks.len(), 3);
        assert_eq!(library.backlinks["page1.md"], set! {PathBuf::from("_index.md")});
        assert_eq!(
            library.backlinks["page2.md"],
            set! {PathBuf::from("page1.md"), PathBuf::from("_index.md")}
        );
        assert_eq!(library.backlinks["_index.md"], set! {PathBuf::from("page2.md")});
    }

    #[test]
    fn can_sort_sections_by_weight() {
        let config = Config::default_for_test();
        let mut library = Library::default();
        let sections = vec![
            ("content/_index.md", "en", 0, false, SortBy::Weight),
            ("content/blog/_index.md", "en", 0, false, SortBy::Weight),
            ("content/novels/_index.md", "en", 3, false, SortBy::Weight),
            ("content/novels/first/_index.md", "en", 2, false, SortBy::Weight),
            ("content/novels/second/_index.md", "en", 1, false, SortBy::Weight),
            // Transparency does not apply to sections as of now!
            ("content/wiki/_index.md", "en", 4, true, SortBy::Weight),
            ("content/wiki/recipes/_index.md", "en", 1, false, SortBy::Weight),
            ("content/wiki/programming/_index.md", "en", 2, false, SortBy::Weight),
        ];
        for (p, l, w, t, s) in sections.clone() {
            library.insert_section(create_section(p, l, w, t, s, None));
        }

        library.populate_sections(&config, Path::new("content"));
        assert_eq!(library.sections.len(), sections.len());
        let root_section = &library.sections[&PathBuf::from("content/_index.md")];
        assert_eq!(root_section.lower, None);
        assert_eq!(root_section.higher, None);

        let blog_section = &library.sections[&PathBuf::from("content/blog/_index.md")];
        assert_eq!(blog_section.lower, None);
        assert_eq!(blog_section.higher, Some(PathBuf::from("content/novels/_index.md")));

        let novels_section = &library.sections[&PathBuf::from("content/novels/_index.md")];
        assert_eq!(novels_section.lower, Some(PathBuf::from("content/blog/_index.md")));
        assert_eq!(novels_section.higher, Some(PathBuf::from("content/wiki/_index.md")));
        assert_eq!(
            novels_section.subsections,
            vec![
                PathBuf::from("content/novels/second/_index.md"),
                PathBuf::from("content/novels/first/_index.md"),
            ]
        );

        let first_novel_section =
            &library.sections[&PathBuf::from("content/novels/first/_index.md")];
        assert_eq!(
            first_novel_section.lower,
            Some(PathBuf::from("content/novels/second/_index.md"))
        );
        assert_eq!(first_novel_section.higher, None);

        let second_novel_section =
            &library.sections[&PathBuf::from("content/novels/second/_index.md")];
        assert_eq!(second_novel_section.lower, None);
        assert_eq!(
            second_novel_section.higher,
            Some(PathBuf::from("content/novels/first/_index.md"))
        );
    }

    #[test]
    fn can_sort_sections_by_title() {
        fn create_section(file_path: &str, title: &str, weight: usize, sort_by: SortBy) -> Section {
            let mut section = Section::default();
            section.lang = "en".to_owned();
            section.file = FileInfo::new_section(Path::new(file_path), &PathBuf::new());
            section.meta.title = Some(title.to_owned());
            section.meta.weight = weight;
            section.meta.transparent = false;
            section.meta.sort_by = sort_by;
            section.meta.page_template = Some("new_page.html".to_owned());
            section
        }

        let config = Config::default_for_test();
        let mut library = Library::default();
        let sections = vec![
            ("content/_index.md", "root", 0, SortBy::Title),
            ("content/a_first/_index.md", "1", 1, SortBy::Title),
            ("content/b_third/_index.md", "3", 2, SortBy::Title),
            ("content/c_second/_index.md", "2", 2, SortBy::Title),
        ];
        for (p, l, w, s) in sections.clone() {
            library.insert_section(create_section(p, l, w, s));
        }

        library.populate_sections(&config, Path::new("content"));
        assert_eq!(library.sections.len(), sections.len());

        let root_section = &library.sections[&PathBuf::from("content/_index.md")];
        assert_eq!(root_section.lower, None);
        assert_eq!(root_section.higher, None);

        let first = &library.sections[&PathBuf::from("content/a_first/_index.md")];
        assert_eq!(first.lower, None);
        assert_eq!(first.higher, Some(PathBuf::from("content/c_second/_index.md")));

        let second = &library.sections[&PathBuf::from("content/c_second/_index.md")];
        assert_eq!(second.lower, Some(PathBuf::from("content/a_first/_index.md")));
        assert_eq!(second.higher, Some(PathBuf::from("content/b_third/_index.md")));

        let third = &library.sections[&PathBuf::from("content/b_third/_index.md")];
        assert_eq!(third.lower, Some(PathBuf::from("content/c_second/_index.md")));
        assert_eq!(third.higher, None);
    }
}
