// Pruebas comunes de un diccionario con la interfaz de la semana 7. Map es un mapa de
// std::string a int: HashMap<int>. Solo usan esa interfaz, sin mirar cómo está hecho por dentro.
#ifndef MAP_SUITE_H
#define MAP_SUITE_H

#include <cassert>
#include <cstddef>
#include <random>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

template <typename Map>
void test_new_is_empty() {
    Map map;
    assert(map.size() == 0);
    assert(map.get("lima") == nullptr);
    assert(!map.contains("lima"));
    assert(!map.remove("lima"));
    assert(map.check_invariants());
}

template <typename Map>
void test_set_and_get() {
    Map map;
    map.set("lima", 1);
    map.set("cusco", 2);
    assert(map.size() == 2);
    assert(*map.get("lima") == 1);
    assert(*map.get("cusco") == 2);
    assert(map.get("puno") == nullptr);
    assert(map.check_invariants());
}

template <typename Map>
void test_overwrite() {
    Map map;
    map.set("lima", 1);
    map.set("lima", 2);
    assert(map.size() == 1);
    assert(*map.get("lima") == 2);
    assert(map.check_invariants());
}

template <typename Map>
void test_contains() {
    Map map;
    map.set("lima", 0);
    assert(map.contains("lima"));
    assert(!map.contains("Lima"));
    assert(!map.contains("lim"));
    assert(!map.contains("limaa"));
}

template <typename Map>
void test_remove_present() {
    Map map;
    map.set("lima", 1);
    map.set("cusco", 2);
    assert(map.remove("lima"));
    assert(map.size() == 1);
    assert(!map.contains("lima"));
    assert(*map.get("cusco") == 2);
    assert(map.check_invariants());
    map.set("lima", 3);
    assert(*map.get("lima") == 3);
    assert(map.size() == 2);
}

template <typename Map>
void test_remove_absent() {
    Map map;
    map.set("lima", 1);
    assert(!map.remove("cusco"));
    assert(map.size() == 1);
    assert(map.remove("lima"));
    assert(!map.remove("lima"));
    assert(map.size() == 0);
    assert(map.check_invariants());
}

template <typename Map>
void test_unusual_keys() {
    Map map;
    map.set("", 1);
    map.set(std::string("a\0b", 3), 2);
    map.set("a", 3);
    map.set("ñandú", 4);
    map.set(std::string(5000, 'x'), 5);
    assert(map.size() == 5);
    assert(*map.get("") == 1);
    assert(*map.get(std::string("a\0b", 3)) == 2);
    assert(*map.get("a") == 3);
    assert(*map.get("ñandú") == 4);
    assert(*map.get(std::string(5000, 'x')) == 5);
    assert(map.check_invariants());
}

template <typename Map>
void test_growth() {
    Map map;
    map.set_max_load_factor(0.5);
    std::size_t initial = map.capacity();
    const int total = 20000;
    for (int i = 0; i < total; i++) {
        map.set("k" + std::to_string(i), i);
        assert(static_cast<double>(map.size()) <= 0.5 * static_cast<double>(map.capacity()));
    }
    assert(map.size() == static_cast<std::size_t>(total));
    assert(map.capacity() > initial);
    for (int i = 0; i < total; i++) {
        assert(*map.get("k" + std::to_string(i)) == i);
    }
    assert(map.check_invariants());
}

template <typename Map>
void test_copy_is_independent() {
    Map original;
    for (int i = 0; i < 100; i++) {
        original.set("k" + std::to_string(i), i);
    }
    Map copy(original);
    copy.set("k0", -1);
    assert(copy.remove("k1"));
    copy.set("nueva", 7);
    assert(*original.get("k0") == 0);
    assert(original.contains("k1"));
    assert(!original.contains("nueva"));
    assert(original.size() == 100);
    assert(copy.size() == 100);
    assert(*copy.get("k50") == 50);
    assert(original.check_invariants());
    assert(copy.check_invariants());

    Map assigned;
    assigned.set("vieja", 1);
    assigned = original;
    assert(!assigned.contains("vieja"));
    assert(assigned.size() == 100);
    assigned.set("k2", -2);
    assert(*original.get("k2") == 2);
}

template <typename Map>
void test_self_assignment() {
    Map map;
    map.set("lima", 1);
    Map& same = map;
    map = same;
    assert(*map.get("lima") == 1);
    assert(map.size() == 1);
    assert(map.check_invariants());
}

template <typename Map>
void test_move_leaves_empty_and_usable() {
    Map source;
    for (int i = 0; i < 50; i++) {
        source.set("k" + std::to_string(i), i);
    }
    Map target(std::move(source));
    assert(target.size() == 50);
    assert(*target.get("k49") == 49);
    assert(source.size() == 0);
    assert(source.get("k1") == nullptr);
    assert(!source.contains("k1"));
    assert(!source.remove("k1"));
    assert(source.check_invariants());
    source.set("otra", 1);
    assert(*source.get("otra") == 1);
    assert(source.check_invariants());

    Map assigned;
    assigned.set("vieja", 1);
    assigned = std::move(target);
    assert(!assigned.contains("vieja"));
    assert(assigned.size() == 50);
    assert(target.size() == 0);
    target.set("de_nuevo", 2);
    assert(*target.get("de_nuevo") == 2);
    assert(assigned.check_invariants());
    assert(target.check_invariants());
}

// Operaciones al azar, con semilla fija, comparando con std::unordered_map.
template <typename Map>
void random_against_unordered_map(unsigned seed, int steps, unsigned key_range) {
    std::mt19937 rng(seed);
    Map map;
    std::unordered_map<std::string, int> model;
    for (int step = 0; step < steps; step++) {
        std::string key = "k" + std::to_string(rng() % key_range);
        switch (rng() % 4) {
            case 0:
            case 1: {
                int value = static_cast<int>(rng() % 1000);
                map.set(key, value);
                model[key] = value;
                break;
            }
            case 2: {
                const int* found = map.get(key);
                auto it = model.find(key);
                assert((found == nullptr) == (it == model.end()));
                if (found != nullptr) {
                    assert(*found == it->second);
                }
                assert(map.contains(key) == (it != model.end()));
                break;
            }
            default:
                assert(map.remove(key) == (model.erase(key) == 1));
                break;
        }
        assert(map.size() == model.size());
        if (step % 1000 == 0) {
            assert(map.check_invariants());
        }
    }
    assert(map.check_invariants());
    for (const auto& entry : model) {
        assert(*map.get(entry.first) == entry.second);
    }
}

template <typename Map>
void test_random_small() {
    // Pocas claves: se borra y se reinserta mucho.
    random_against_unordered_map<Map>(1u, 30000, 50);
}

template <typename Map>
void test_random_large() {
    // Muchas claves: la tabla tiene que crecer varias veces.
    random_against_unordered_map<Map>(2u, 40000, 20000);
}

template <typename Map>
void run_map_suite() {
    test_new_is_empty<Map>();
    test_set_and_get<Map>();
    test_overwrite<Map>();
    test_contains<Map>();
    test_remove_present<Map>();
    test_remove_absent<Map>();
    test_unusual_keys<Map>();
    test_growth<Map>();
    test_copy_is_independent<Map>();
    test_self_assignment<Map>();
    test_move_leaves_empty_and_usable<Map>();
    test_random_small<Map>();
    test_random_large<Map>();
}

#endif
