use super::{
    CaseSensitiveLevTrie, DamerauLevenshteinDistanceCalculator, KeyChecker, LevTrie,
    LevenshteinDistanceCalculator, SearchResult,
};
use std::collections::BTreeMap;
use std::fmt::Display;
use std::io::Write;

struct ExpectedResults {
    data: Vec<(usize, usize)>,
}

impl ExpectedResults {
    fn new(id_map: &BTreeMap<String, usize>, results: &[(String, usize)]) -> ExpectedResults {
        let data = results
            .iter()
            .map(|(key, distance)| {
                (
                    *id_map
                        .get(key)
                        .ok_or_else(|| format!("Id not found for key '{key}'"))
                        .unwrap(),
                    *distance,
                )
            })
            .collect::<Vec<(usize, usize)>>();
        ExpectedResults { data }
    }

    fn check(&self, search_results: &[SearchResult]) {
        for i in 0..self.data.len() {
            let SearchResult { word, distance } = search_results[i];
            let data = self.data[i];
            if data != (word, distance) {
                panic!("({}, {}) <> ({}, {})", data.0, data.1, word, distance);
            }
        }
    }
}

fn print_search_results<T: Display + Copy, C: KeyChecker<T>>(
    trie: &LevTrie<T, C>,
    search_results: &[SearchResult],
    key_separator: &str,
) -> Result<(), std::io::Error> {
    for result in search_results {
        let mut word = Vec::<u8>::new();
        for (i, fragment) in trie.lineal_descendant(result.word).enumerate() {
            if i > 0 {
                word.write(format!("{}{}", key_separator, fragment).as_bytes())?;
            } else {
                word.write(format!("{}", fragment).as_bytes())?;
            }
        }
        println!(
            "distance: {}, wordkey: {}, id: {}",
            result.distance,
            String::from_utf8(word).unwrap(),
            result.word
        );
    }
    Ok(())
}

const WORDLIST: [&str; 16] = [
    "skyscraper",
    "camel",
    "coal",
    "caos",
    "copper",
    "hello",
    "Bugis",
    "Kembangan",
    "Singapore",
    "Fullerton",
    "Lavender",
    "aircraft",
    "boat",
    "ship",
    "cargo",
    "tanker",
];

#[test]
fn test_damerau_levenshtein_strings() {
    let mut trie: CaseSensitiveLevTrie = LevTrie::new();
    let mut id_map = BTreeMap::<String, usize>::new();
    for word in WORDLIST {
        let (_, id) = trie.add(word.chars());
        id_map.insert(String::from(word), id);
    }
    let results = trie.fuzzy_search::<DamerauLevenshteinDistanceCalculator>(
        &"coat".chars().collect::<Vec<char>>(),
        6,
    );

    print_search_results(&trie, &results, "").unwrap();

    let expected_results = ExpectedResults::new(
        &id_map,
        &[
            (String::from("coal"), 1),
            (String::from("boat"), 1),
            (String::from("caos"), 2),
            (String::from("camel"), 4),
            (String::from("copper"), 4),
            (String::from("ship"), 4),
        ],
    );
    expected_results.check(&results);
}

#[test]
fn test_levenshtein_strings() {
    let mut trie: CaseSensitiveLevTrie = LevTrie::new();
    let mut id_map = BTreeMap::<String, usize>::new();
    for word in WORDLIST {
        let (_, id) = trie.add(word.chars());
        id_map.insert(String::from(word), id);
    }
    let results = trie
        .fuzzy_search::<LevenshteinDistanceCalculator>(&"coat".chars().collect::<Vec<char>>(), 6);

    print_search_results(&trie, &results, "").unwrap();

    let expected_results = ExpectedResults::new(
        &id_map,
        &[
            (String::from("coal"), 1),
            (String::from("boat"), 1),
            (String::from("caos"), 3),
            (String::from("camel"), 4),
            (String::from("copper"), 4),
            (String::from("ship"), 4),
        ],
    );
    expected_results.check(&results);
}
