// Corre las pruebas de la sesión 2: spell_tests <nombre> <carpeta de fixtures>.
#include <algorithm>
#include <cassert>
#include <cstring>
#include <iostream>
#include <set>
#include <string>
#include <vector>

#include "dictionaries.h"
#include "load_words.h"
#include "spell_checker.h"

namespace {

std::string fixtures;

// Comprueba que una cadena sea UTF-8 válido, carácter por carácter.
bool valid_utf8(const std::string& word) {
    std::size_t i = 0;
    while (i < word.size()) {
        const std::size_t length = utf8_length(word[i]);
        if (length == 0 || i + length > word.size()) {
            return false;
        }
        for (std::size_t j = 1; j < length; j++) {
            if ((static_cast<unsigned char>(word[i + j]) & 0xC0) != 0x80) {
                return false;
            }
        }
        i += length;
    }
    return true;
}

bool has(const std::vector<std::string>& list, const std::string& word) {
    return std::find(list.begin(), list.end(), word) != list.end();
}

std::vector<std::string> dictionary() { return load_lines(fixtures + "/mini_dict.txt"); }

void helper_splits_letters() {
    const std::vector<std::string> expected = {"a", "ñ", "o"};
    assert(split_letters("año") == expected);
    assert(split_letters("año").size() == 3);
    assert(std::string("año").size() == 4);
    assert(join_letters(split_letters("pingüino")) == "pingüino");
}

void helper_extracts_words() {
    const std::vector<std::string> expected = {"la", "casa", "del", "niño", "y", "el", "águila"};
    assert(extract_words("La CASA del NIÑO, ¿y el Águila?") == expected);
}

void candidates_are_the_three_edits() {
    const std::vector<std::string> edits = one_edit_candidates("sol");
    assert(has(edits, "ol"));    // borrar
    assert(has(edits, "so"));
    assert(has(edits, "sal"));   // reemplazar
    assert(has(edits, "sól"));
    assert(has(edits, "asol"));  // insertar al inicio
    assert(has(edits, "soñl"));  // en medio
    assert(has(edits, "solé"));  // al final
    assert(!has(edits, "sol"));
    assert(!has(edits, "slo"));  // intercambiar son dos cambios
    // 3 borrados, 3 * 32 reemplazos y 4 * 33 inserciones.
    assert(edits.size() == 3 + 3 * 32 + 4 * 33);
}

void candidates_are_valid_utf8() {
    for (const char* word : {"año", "café", "pingüino", "ñ"}) {
        for (const std::string& candidate : one_edit_candidates(word)) {
            assert(valid_utf8(candidate));
        }
    }
    // Borrar y reemplazar cuentan letras, no bytes.
    assert(has(one_edit_candidates("año"), "ao"));
    assert(has(one_edit_candidates("año"), "ano"));
    assert(has(one_edit_candidates("cafe"), "café"));
    assert(one_edit_candidates("ñ").size() == 1 + 32 + 2 * 33);
}

template <typename Dict>
void check_dict(const Dict& dict) {
    assert(dict.contains("casa"));
    assert(dict.contains("niño"));
    assert(!dict.contains("niño "));
    assert(!dict.contains("cas"));
    const std::vector<std::string> near_cafe = {"café"};
    assert(suggest(dict, "cafe") == near_cafe);
    const std::vector<std::string> near_cosa = {"casa", "caso"};
    assert(suggest(dict, "cosq") == std::vector<std::string>{"cosa"});
    assert(suggest(dict, "cas") == near_cosa);
    assert(suggest(dict, "xxxxxxx").empty());
    // Nunca se sugiere la cadena vacía, aunque borrar dé "".
    assert(suggest(dict, "z").empty());
}

void suggest_with_sorted() { check_dict(SortedDict(dictionary())); }
void suggest_with_std() { check_dict(StdDict(dictionary(), 1.0f)); }
void suggest_with_probing() { check_dict(TableDict<ProbingHashMap>(dictionary(), 0.7)); }
void suggest_with_week7() { check_dict(TableDict<HashMap>(dictionary(), 1.0)); }

// Las sugerencias salen en orden de bytes: ñ y las vocales con tilde van después de la z.
void suggestions_are_sorted_and_unique() {
    const TableDict<ProbingHashMap> dict(dictionary(), 0.7);
    const std::vector<std::string> near = suggest(dict, "ano");
    assert(near == std::vector<std::string>{"año"});
    const std::vector<std::string> mesa = suggest(dict, "mes");
    assert(mesa == std::vector<std::string>{"mesa"});
    const std::vector<std::string> sal = suggest(dict, "sl");
    const std::vector<std::string> both = {"sal", "sol"};
    assert(sal == both);
}

template <typename Dict>
void check_report(const Dict& dict) {
    const std::vector<std::string> words = extract_words(load_text(fixtures + "/mini_text.txt"));
    const std::vector<Misspelling> report = check_words(dict, words);
    std::set<std::string> distinct;
    for (const Misspelling& item : report) {
        assert(distinct.insert(item.word).second);
        assert(!dict.contains(item.word));
    }
    // "cafe" aparece dos veces y "casq" una. "la", "una", "tiene"... tampoco están en el fixture.
    bool found_cafe = false;
    bool found_casq = false;
    for (const Misspelling& item : report) {
        if (item.word == "cafe") {
            found_cafe = true;
            assert(item.count == 2);
            assert(item.suggestions == std::vector<std::string>{"café"});
        }
        if (item.word == "casq") {
            found_casq = true;
            assert(item.count == 1);
            const std::vector<std::string> expected = {"casa", "caso"};
            assert(item.suggestions == expected);
        }
        assert(item.word != "casa" && item.word != "niño" && item.word != "perro");
    }
    assert(found_cafe && found_casq);
    assert(report.front().word == "la");
}

void report_with_probing() { check_report(TableDict<ProbingHashMap>(dictionary(), 0.7)); }
void report_with_week7() { check_report(TableDict<HashMap>(dictionary(), 1.0)); }
void report_with_std() { check_report(StdDict(dictionary(), 1.0f)); }
void report_with_sorted() { check_report(SortedDict(dictionary())); }

// Un diccionario que cuenta cuántas veces se le pregunta.
struct CountingDict {
    TableDict<ProbingHashMap> dict;
    mutable std::size_t queries = 0;
    bool contains(const std::string& word) const {
        queries++;
        return dict.contains(word);
    }
};

void suggests_each_word_once() {
    CountingDict dict{TableDict<ProbingHashMap>(dictionary(), 0.7)};
    const std::vector<std::string> one = {"zzz"};
    check_words(dict, one);
    const std::size_t once = dict.queries;
    assert(once > 1);
    dict.queries = 0;
    const std::vector<std::string> many(100, "zzz");
    const std::vector<Misspelling> report = check_words(dict, many);
    assert(report.size() == 1 && report[0].count == 100);
    // Se revisan las 100 apariciones, pero se calculan las sugerencias una sola vez.
    assert(dict.queries == once + 99);
}

struct Test {
    const char* name;
    void (*run)();
};

const Test kTests[] = {
    {"helper_splits_letters", helper_splits_letters},
    {"helper_extracts_words", helper_extracts_words},
    {"candidates_are_the_three_edits", candidates_are_the_three_edits},
    {"candidates_are_valid_utf8", candidates_are_valid_utf8},
    {"suggest_with_sorted", suggest_with_sorted},
    {"suggest_with_std", suggest_with_std},
    {"suggest_with_probing", suggest_with_probing},
    {"suggest_with_week7", suggest_with_week7},
    {"suggestions_are_sorted_and_unique", suggestions_are_sorted_and_unique},
    {"report_with_probing", report_with_probing},
    {"report_with_week7", report_with_week7},
    {"report_with_std", report_with_std},
    {"report_with_sorted", report_with_sorted},
    {"suggests_each_word_once", suggests_each_word_once},
};

}  // namespace

int main(int argc, char** argv) {
    if (argc < 3) {
        for (const Test& test : kTests) {
            std::cout << test.name << "\n";
        }
        return 0;
    }
    fixtures = argv[2];
    for (const Test& test : kTests) {
        if (std::strcmp(test.name, argv[1]) == 0) {
            test.run();
            return 0;
        }
    }
    std::cerr << "no existe la prueba " << argv[1] << "\n";
    return 2;
}
