use liwe::graph::Graph;
use liwe::model::Key;
use liwe::query::execute;
use liwe::query::prelude::{count, filter, find, includes, key_eq, or, references};
use liwe::query::{Filter, FindMatch, FindOp, InclusionAnchor, Outcome, ReferenceAnchor};

pub fn all_keys(graph: &Graph) -> Vec<FindMatch> {
    match execute(&find(FindOp::new()), graph) {
        Outcome::Find { matches } => matches,
        _ => unreachable!("Find returns Find"),
    }
}

pub fn key_exists(graph: &Graph, key: &Key) -> bool {
    match execute(&count(filter(key_eq(key.to_string()))), graph) {
        Outcome::Count(n) => n > 0,
        _ => unreachable!("Count returns Count"),
    }
}

pub fn inclusion_backlinks(graph: &Graph, key: &Key) -> Vec<Key> {
    keys_of(execute(&find(filter(includes_direct(key))), graph))
}

pub fn reference_backlinks(graph: &Graph, key: &Key) -> Vec<Key> {
    keys_of(execute(&find(filter(references_direct(key))), graph))
}

pub fn all_backlinks(graph: &Graph, key: &Key) -> Vec<Key> {
    let f = or(vec![includes_direct(key), references_direct(key)]);
    keys_of(execute(&find(filter(f)), graph))
}

pub fn inclusion_count(graph: &Graph, key: &Key) -> usize {
    count_of(execute(&count(filter(includes_direct(key))), graph))
}

pub fn reference_count(graph: &Graph, key: &Key) -> usize {
    count_of(execute(&count(filter(references_direct(key))), graph))
}

fn includes_direct(key: &Key) -> Filter {
    includes(InclusionAnchor::new(key.to_string(), 1, 1))
}

fn references_direct(key: &Key) -> Filter {
    references(ReferenceAnchor::new(key.to_string(), 1, 1))
}

fn keys_of(outcome: Outcome) -> Vec<Key> {
    match outcome {
        Outcome::Find { matches } => matches.into_iter().map(|m| m.key).collect(),
        _ => unreachable!("Find returns Find"),
    }
}

fn count_of(outcome: Outcome) -> usize {
    match outcome {
        Outcome::Count(n) => n,
        _ => unreachable!("Count returns Count"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use indoc::indoc;
    use itertools::Itertools;
    use liwe::graph::{Graph, GraphContext};
    use liwe::model::config::MarkdownOptions;
    use liwe::model::node::NodePointer;
    use liwe::state::from_indoc;

    fn make_graph(docs: &str) -> Graph {
        Graph::import(&from_indoc(docs), MarkdownOptions::default(), None)
    }

    fn sorted(mut keys: Vec<Key>) -> Vec<String> {
        keys.sort_by(|a, b| a.to_string().cmp(&b.to_string()));
        keys.into_iter().map(|k| k.to_string()).collect()
    }

    fn graph_inclusion_keys(graph: &Graph, key: &Key) -> Vec<String> {
        graph
            .get_inclusion_edges_to(key)
            .into_iter()
            .map(|id| graph.node(id).node_key())
            .filter(|k| k != key)
            .unique()
            .map(|k| k.to_string())
            .sorted()
            .collect()
    }

    fn graph_reference_keys(graph: &Graph, key: &Key) -> Vec<String> {
        graph
            .get_reference_edges_to(key)
            .into_iter()
            .map(|id| graph.node(id).node_key())
            .filter(|k| k != key)
            .unique()
            .map(|k| k.to_string())
            .sorted()
            .collect()
    }

    const FIXTURE: &str = indoc! {"
        [b](2)
        _
        [c](3)
        _
        # C

        See [other](2) for details.
    "};

    #[test]
    fn all_keys_matches_graph_keys() {
        let graph = make_graph(FIXTURE);
        let helper: Vec<String> = all_keys(&graph)
            .into_iter()
            .map(|m| m.key.to_string())
            .sorted()
            .collect();
        let direct: Vec<String> = graph
            .keys()
            .into_iter()
            .map(|k| k.to_string())
            .sorted()
            .collect();
        assert_eq!(helper, direct);
    }

    #[test]
    fn key_exists_true_for_present_key() {
        let graph = make_graph(FIXTURE);
        assert!(key_exists(&graph, &Key::name("1")));
        assert!(key_exists(&graph, &Key::name("3")));
    }

    #[test]
    fn key_exists_false_for_missing_key() {
        let graph = make_graph(FIXTURE);
        assert!(!key_exists(&graph, &Key::name("does-not-exist")));
    }

    #[test]
    fn inclusion_backlinks_match_graph_edges() {
        let graph = make_graph(FIXTURE);
        let target = Key::name("2");
        assert_eq!(
            sorted(inclusion_backlinks(&graph, &target)),
            graph_inclusion_keys(&graph, &target),
        );
    }

    #[test]
    fn reference_backlinks_match_graph_edges() {
        let graph = make_graph(FIXTURE);
        let target = Key::name("2");
        assert_eq!(
            sorted(reference_backlinks(&graph, &target)),
            graph_reference_keys(&graph, &target),
        );
    }

    #[test]
    fn all_backlinks_unions_both_kinds() {
        let graph = make_graph(FIXTURE);
        let target = Key::name("2");
        let helper = sorted(all_backlinks(&graph, &target));
        let mut combined: Vec<String> = graph_inclusion_keys(&graph, &target)
            .into_iter()
            .chain(graph_reference_keys(&graph, &target))
            .unique()
            .collect();
        combined.sort();
        assert_eq!(helper, combined);
    }

    #[test]
    fn inclusion_count_matches_unique_sources() {
        let graph = make_graph(FIXTURE);
        let target = Key::name("3");
        assert_eq!(
            inclusion_count(&graph, &target),
            graph_inclusion_keys(&graph, &target).len(),
        );
    }

    #[test]
    fn reference_count_matches_unique_sources() {
        let graph = make_graph(FIXTURE);
        let target = Key::name("2");
        assert_eq!(
            reference_count(&graph, &target),
            graph_reference_keys(&graph, &target).len(),
        );
    }
}
