use std::collections::BTreeMap;

use crate::mit::lib::author::Author;
use std::convert::TryFrom;
use thiserror::Error;

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Authors {
    pub authors: BTreeMap<String, Author>,
}

impl Authors {
    #[must_use]
    pub fn new(authors: BTreeMap<String, Author>) -> Authors {
        Authors { authors }
    }

    #[must_use]
    pub fn get(&self, author_initials: &[&str]) -> Vec<Option<&Author>> {
        author_initials
            .iter()
            .map(|initial| self.authors.get(*initial))
            .collect()
    }

    #[must_use]
    pub fn merge(&self, authors: &Authors) -> Authors {
        Authors {
            authors: authors
                .authors
                .iter()
                .fold(self.authors.clone(), |mut acc, (key, value)| {
                    acc.insert(key.clone(), value.clone());
                    acc
                }),
        }
    }

    #[must_use]
    pub fn example() -> Authors {
        let mut store = BTreeMap::new();
        store.insert(
            "ae".into(),
            Author::new("Anyone Else", "anyone@example.com", None),
        );
        store.insert(
            "se".into(),
            Author::new("Someone Else", "someone@example.com", None),
        );
        store.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", Some("0A46826A")),
        );
        Authors::new(store)
    }
}

impl TryFrom<&str> for Authors {
    type Error = ConfigParseError;

    fn try_from(input: &str) -> Result<Self, Self::Error> {
        serde_yaml::from_str(input)
            .or_else(|yaml_error| {
                toml::from_str(input)
                    .map_err(|toml_error| ConfigParseError::Parse(yaml_error, toml_error))
            })
            .map_err(ConfigParseError::from)
            .map(Authors::new)
    }
}

#[derive(Error, Debug)]
pub enum ConfigParseError {
    #[error("failed to parse authors as toml {0} or as yaml {1}")]
    Parse(serde_yaml::Error, toml::de::Error),
    #[error("failed to serialise toml {0}")]
    SerialiseYaml(#[from] toml::ser::Error),
}

impl TryFrom<Authors> for String {
    type Error = ConfigParseError;

    fn try_from(value: Authors) -> Result<Self, Self::Error> {
        toml::to_string(&value.authors).map_err(ConfigParseError::from)
    }
}

#[cfg(test)]
mod tests_authors {
    #![allow(clippy::wildcard_imports)]

    use super::*;
    use crate::mit::lib::author::Author;
    use indoc::indoc;
    use std::convert::TryInto;

    #[test]
    fn it_can_get_an_author_in_it() {
        let mut store = BTreeMap::new();
        store.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        let actual = Authors::new(store);

        assert_eq!(
            actual.get(&["bt"]),
            vec![Some(&Author::new(
                "Billie Thompson",
                "billie@example.com",
                None,
            ))]
        )
    }

    #[test]
    fn i_can_get_multiple_authors_out_at_the_same_time() {
        let mut store: BTreeMap<String, Author> = BTreeMap::new();
        store.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        store.insert(
            "se".into(),
            Author::new("Somebody Else", "somebody@example.com", None),
        );
        let actual = Authors::new(store);

        assert_eq!(
            actual.get(&["bt"]),
            vec![Some(&Author::new(
                "Billie Thompson",
                "billie@example.com",
                None,
            ))]
        );
        assert_eq!(
            actual.get(&["se"]),
            vec![Some(&Author::new(
                "Somebody Else",
                "somebody@example.com",
                None,
            ))]
        )
    }

    #[test]
    fn there_is_an_example_constructor() {
        let mut store = BTreeMap::new();
        store.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", Some("0A46826A")),
        );
        store.insert(
            "se".into(),
            Author::new("Someone Else", "someone@example.com", None),
        );
        store.insert(
            "ae".into(),
            Author::new("Anyone Else", "anyone@example.com", None),
        );
        let expected = Authors::new(store);

        assert_eq!(Authors::example(), expected,)
    }

    #[test]
    fn merge_multiple_authors_together() {
        let mut map1: BTreeMap<String, Author> = BTreeMap::new();
        map1.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        map1.insert(
            "se".into(),
            Author::new("Someone Else", "someone@example.com", None),
        );
        let input1: Authors = Authors::new(map1);

        let mut map2: BTreeMap<String, Author> = BTreeMap::new();
        map2.insert(
            "bt".into(),
            Author::new("Billie Thompson", "bt@example.com", None),
        );
        map2.insert(
            "ae".into(),
            Author::new("Anyone Else", "anyone@example.com", None),
        );
        let input2: Authors = Authors::new(map2);

        let mut expected_map: BTreeMap<String, Author> = BTreeMap::new();

        expected_map.insert(
            "bt".into(),
            Author::new("Billie Thompson", "bt@example.com", None),
        );
        expected_map.insert(
            "se".into(),
            Author::new("Someone Else", "someone@example.com", None),
        );
        expected_map.insert(
            "ae".into(),
            Author::new("Anyone Else", "anyone@example.com", None),
        );

        let expected: Authors = Authors::new(expected_map);

        assert_eq!(expected, input1.merge(&input2));
    }

    #[test]
    fn must_be_valid_yaml() {
        let actual: Result<_, _> = Authors::try_from("Hello I am invalid yaml : : :");
        assert_eq!(true, actual.is_err())
    }

    #[test]
    fn it_can_parse_a_standard_toml_file() {
        let actual = Authors::try_from(indoc!(
            "
            [bt]
            name = \"Billie Thompson\"
            email = \"billie@example.com\"
            "
        ))
        .expect("Failed to parse yaml");

        let mut input: BTreeMap<String, Author> = BTreeMap::new();
        input.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        let expected = Authors::new(input);

        assert_eq!(expected, actual);
    }

    #[test]
    fn it_can_parse_a_standard_yaml_file() {
        let actual = Authors::try_from(indoc!(
            "
            ---
            bt:
                name: Billie Thompson
                email: billie@example.com
            "
        ))
        .expect("Failed to parse yaml");

        let mut input: BTreeMap<String, Author> = BTreeMap::new();
        input.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        let expected = Authors::new(input);

        assert_eq!(expected, actual);
    }

    #[test]
    fn yaml_files_can_contain_signing_keys() {
        let actual = Authors::try_from(indoc!(
            "
            ---
            bt:
                name: Billie Thompson
                email: billie@example.com
                signingkey: 0A46826A
            "
        ))
        .expect("Failed to parse yaml");

        let mut expected_authors: BTreeMap<String, Author> = BTreeMap::new();
        expected_authors.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", Some("0A46826A")),
        );
        let expected = Authors::new(expected_authors);

        assert_eq!(expected, actual);
    }

    #[test]
    fn it_converts_to_standard_toml() {
        let mut map: BTreeMap<String, Author> = BTreeMap::new();
        map.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", None),
        );
        let actual: String = Authors::new(map).try_into().unwrap();
        let expected = indoc!(
            "
            [bt]
            name = \"Billie Thompson\"
            email = \"billie@example.com\"
            "
        )
        .to_string();

        assert_eq!(expected, actual);
    }

    #[test]
    fn it_includes_the_signing_key_if_set() {
        let mut map: BTreeMap<String, Author> = BTreeMap::new();
        map.insert(
            "bt".into(),
            Author::new("Billie Thompson", "billie@example.com", Some("0A46826A")),
        );
        let actual: String = Authors::new(map).try_into().unwrap();
        let expected = indoc!(
            "
            [bt]
            name = \"Billie Thompson\"
            email = \"billie@example.com\"
            signingkey = \"0A46826A\"
            "
        )
        .to_string();

        assert_eq!(expected, actual);
    }
}
