#include "maxminddb_test_helper.h"

MMDB_entry_data_list_s *
test_array_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *array = entry_data_list = entry_data_list->next;
    cmp_ok(array->entry_data.type,
           "==",
           MMDB_DATA_TYPE_ARRAY,
           "'array' key's value is an array");
    cmp_ok(array->entry_data.data_size,
           "==",
           3,
           "'array' key's value has 3 elements");

    MMDB_entry_data_list_s *idx0 = entry_data_list = entry_data_list->next;
    cmp_ok(idx0->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "first array entry is a UINT32");
    cmp_ok(idx0->entry_data.uint32, "==", 1, "first array entry value is 1");

    MMDB_entry_data_list_s *idx1 = entry_data_list = entry_data_list->next;
    cmp_ok(idx1->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "second array entry is a UINT32");
    cmp_ok(idx1->entry_data.uint32, "==", 2, "second array entry value is 2");

    MMDB_entry_data_list_s *idx2 = entry_data_list = entry_data_list->next;
    cmp_ok(idx2->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "third array entry is a UINT32");
    cmp_ok(idx2->entry_data.uint32, "==", 3, "third array entry value is 3");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_boolean_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_BOOLEAN,
           "'boolean' key's value is a boolean");
    ok(value->entry_data.boolean, "'boolean' key's value is true");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_bytes_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_BYTES,
           "'bytes' key's value is bytes");
    uint8_t *bytes = malloc(value->entry_data.data_size);
    if (NULL == bytes) {
        BAIL_OUT("malloc failed");
    }
    memcpy(bytes, value->entry_data.bytes, value->entry_data.data_size);
    uint8_t expect[] = {0x00, 0x00, 0x00, 0x2a};

    ok(memcmp(bytes, expect, 4) == 0, "got expected value for bytes key");

    free((void *)bytes);

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_double_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_DOUBLE,
           "'double' key's value is a double");

    compare_double(value->entry_data.double_value, 42.123456);

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_float_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_FLOAT,
           "'float' key's value is a float");

    compare_float(value->entry_data.float_value, 1.1F);

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_int32_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_INT32,
           "'int32' key's value is an int32");

    int32_t expect = 1 << 28;
    expect *= -1;
    cmp_ok(value->entry_data.int32,
           "==",
           expect,
           "got expected value for int32 key");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_arrayX_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *arrayX = entry_data_list = entry_data_list->next;
    cmp_ok(arrayX->entry_data.type,
           "==",
           MMDB_DATA_TYPE_ARRAY,
           "'map{mapX}{arrayX}' key's value is an array");
    cmp_ok(arrayX->entry_data.data_size,
           "==",
           3,
           "'map{mapX}{arrayX}' key's value has 3 elements");

    MMDB_entry_data_list_s *idx0 = entry_data_list = entry_data_list->next;
    cmp_ok(idx0->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "first array entry is a UINT32");
    cmp_ok(idx0->entry_data.uint32, "==", 7, "first array entry value is 7");

    MMDB_entry_data_list_s *idx1 = entry_data_list = entry_data_list->next;
    cmp_ok(idx1->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "second array entry is a UINT32");
    cmp_ok(idx1->entry_data.uint32, "==", 8, "second array entry value is 8");

    MMDB_entry_data_list_s *idx2 = entry_data_list = entry_data_list->next;
    cmp_ok(idx2->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "third array entry is a UINT32");
    cmp_ok(idx2->entry_data.uint32, "==", 9, "third array entry value is 9");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_mapX_key_value_pair(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *mapX_key = entry_data_list = entry_data_list->next;
    cmp_ok(mapX_key->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UTF8_STRING,
           "found a map key in 'map{mapX}'");
    const char *mapX_key_name = dup_entry_string_or_bail(mapX_key->entry_data);

    if (strcmp(mapX_key_name, "utf8_stringX") == 0) {
        MMDB_entry_data_list_s *mapX_value = entry_data_list =
            entry_data_list->next;
        cmp_ok(mapX_value->entry_data.type,
               "==",
               MMDB_DATA_TYPE_UTF8_STRING,
               "'map{mapX}{utf8_stringX}' type is utf8_string");
        const char *utf8_stringX_value =
            dup_entry_string_or_bail(mapX_value->entry_data);
        ok(strcmp(utf8_stringX_value, "hello") == 0,
           "map{mapX}{utf8_stringX} value is 'hello'");
        free((void *)utf8_stringX_value);
    } else if (strcmp(mapX_key_name, "arrayX") == 0) {
        entry_data_list = test_arrayX_value(entry_data_list);
    } else {
        ok(0, "unknown key found in map{mapX} - %s", mapX_key_name);
    }

    free((void *)mapX_key_name);

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_map_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *map = entry_data_list = entry_data_list->next;
    cmp_ok(map->entry_data.type,
           "==",
           MMDB_DATA_TYPE_MAP,
           "'map' key's value is a map");
    cmp_ok(map->entry_data.data_size,
           "==",
           1,
           "'map' key's value has 1 key/value pair");

    MMDB_entry_data_list_s *map_key_1 = entry_data_list = entry_data_list->next;
    cmp_ok(map_key_1->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UTF8_STRING,
           "found a map key in 'map'");
    const char *map_key_1_name =
        dup_entry_string_or_bail(map_key_1->entry_data);
    ok(strcmp(map_key_1_name, "mapX") == 0, "key name is mapX");
    free((void *)map_key_1_name);

    MMDB_entry_data_list_s *mapX = entry_data_list = entry_data_list->next;
    cmp_ok(mapX->entry_data.type,
           "==",
           MMDB_DATA_TYPE_MAP,
           "'map{mapX}' key's value is a map");
    cmp_ok(mapX->entry_data.data_size,
           "==",
           2,
           "'map' key's value has 2 key/value pairs");

    entry_data_list = test_mapX_key_value_pair(entry_data_list);
    entry_data_list = test_mapX_key_value_pair(entry_data_list);

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_uint128_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT128,
           "'uint128' key's value is an uint128");

#if MMDB_UINT128_IS_BYTE_ARRAY
    uint8_t expect[16] = {0x01,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00,
                          0x00};
    ok(memcmp(value->entry_data.uint128, expect, 16) == 0,
       "uint128 field is 2**120");
#else
    mmdb_uint128_t expect = 1;
    expect <<= 120;
    cmp_ok(value->entry_data.uint128, "==", expect, "uint128 field is 2**120");
#endif

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_uint16_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT16,
           "'uint16' key's value is an uint16");
    uint16_t expect = 100;
    ok(value->entry_data.uint16 == expect, "uint16 field is 100");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_uint32_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "'uint32' key's value is an uint32");
    uint32_t expect = 1 << 28;
    cmp_ok(value->entry_data.uint32, "==", expect, "uint32 field is 100");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_uint64_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT64,
           "'uint64' key's value is an uint64");
    uint64_t expect = 1;
    expect <<= 60;
    cmp_ok(value->entry_data.uint64, "==", expect, "uint64 field is 2**60");

    return entry_data_list;
}

MMDB_entry_data_list_s *
test_utf8_string_value(MMDB_entry_data_list_s *entry_data_list) {
    MMDB_entry_data_list_s *value = entry_data_list = entry_data_list->next;

    cmp_ok(value->entry_data.type,
           "==",
           MMDB_DATA_TYPE_UTF8_STRING,
           "'utf8_string' key's value is a string");
    const char *utf8_string = dup_entry_string_or_bail(value->entry_data);
    // This is hex for "unicode! ☯ - ♫" as bytes
    char expect[19] = {0x75,
                       0x6e,
                       0x69,
                       0x63,
                       0x6f,
                       0x64,
                       0x65,
                       0x21,
                       0x20,
                       0xe2,
                       0x98,
                       0xaf,
                       0x20,
                       0x2d,
                       0x20,
                       0xe2,
                       0x99,
                       0xab,
                       0x00};

    is(utf8_string, expect, "got expected value for utf8_string key");

    free((void *)utf8_string);

    return entry_data_list;
}

void run_tests(int mode, const char *description) {
    const char *filename = "MaxMind-DB-test-decoder.mmdb";
    const char *path = test_database_path(filename);
    MMDB_s *mmdb = open_ok(path, mode, description);
    free((void *)path);

    char *ip = "1.1.1.1";
    MMDB_lookup_result_s result =
        lookup_string_ok(mmdb, ip, filename, description);

    MMDB_entry_data_list_s *entry_data_list, *first;
    int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);

    if (MMDB_SUCCESS != status) {
        BAIL_OUT("MMDB_get_entry_data_list failed with %s",
                 MMDB_strerror(status));
    } else {
        cmp_ok(
            status, "==", MMDB_SUCCESS, "MMDB_get_entry_data_list succeeded");
    }

    first = entry_data_list;

    cmp_ok(entry_data_list->entry_data.type,
           "==",
           MMDB_DATA_TYPE_MAP,
           "first entry in entry data list is a map");
    cmp_ok(entry_data_list->entry_data.data_size,
           "==",
           12,
           "first map in entry data list has 12 k/v pairs");

    while (1) {
        MMDB_entry_data_list_s *key = entry_data_list = entry_data_list->next;

        if (!key) {
            break;
        }

        cmp_ok(key->entry_data.type,
               "==",
               MMDB_DATA_TYPE_UTF8_STRING,
               "found a map key");

        const char *key_name = dup_entry_string_or_bail(key->entry_data);
        if (strcmp(key_name, "array") == 0) {
            entry_data_list = test_array_value(entry_data_list);
        } else if (strcmp(key_name, "boolean") == 0) {
            entry_data_list = test_boolean_value(entry_data_list);
        } else if (strcmp(key_name, "bytes") == 0) {
            entry_data_list = test_bytes_value(entry_data_list);
        } else if (strcmp(key_name, "double") == 0) {
            entry_data_list = test_double_value(entry_data_list);
        } else if (strcmp(key_name, "float") == 0) {
            entry_data_list = test_float_value(entry_data_list);
        } else if (strcmp(key_name, "int32") == 0) {
            entry_data_list = test_int32_value(entry_data_list);
        } else if (strcmp(key_name, "map") == 0) {
            entry_data_list = test_map_value(entry_data_list);
        } else if (strcmp(key_name, "uint128") == 0) {
            entry_data_list = test_uint128_value(entry_data_list);
        } else if (strcmp(key_name, "uint16") == 0) {
            entry_data_list = test_uint16_value(entry_data_list);
        } else if (strcmp(key_name, "uint32") == 0) {
            entry_data_list = test_uint32_value(entry_data_list);
        } else if (strcmp(key_name, "uint64") == 0) {
            entry_data_list = test_uint64_value(entry_data_list);
        } else if (strcmp(key_name, "utf8_string") == 0) {
            entry_data_list = test_utf8_string_value(entry_data_list);
        } else {
            ok(0, "unknown key found in map - %s", key_name);
        }

        free((void *)key_name);
    }

    MMDB_free_entry_data_list(first);

    MMDB_close(mmdb);
    free(mmdb);
}

int main(void) {
    plan(NO_PLAN);
    for_all_modes(&run_tests);
    done_testing();
}
