#include "maxminddb_test_helper.h"

void test_array_0_result(int status,
                         MMDB_entry_data_s entry_data,
                         char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - array[0]",
           function);
    ok(entry_data.has_data, "found a value for array[0]");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - array[0]");
    cmp_ok(entry_data.uint32, "==", 1, "entry value is 1 - array[0]");
}

void test_array_2_result(int status,
                         MMDB_entry_data_s entry_data,
                         char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - array[2]",
           function);
    ok(entry_data.has_data, "found a value for array[2]");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - array[2]");
    cmp_ok(entry_data.uint32, "==", 3, "entry value is 3 - array[2]");
}

void test_array_minus_3_result(int status,
                               MMDB_entry_data_s entry_data,
                               char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - array[-3]",
           function);
    ok(entry_data.has_data, "found a value for array[-3]");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - array[-3]");
    cmp_ok(entry_data.uint32, "==", 1, "entry value is 1 - array[-3]");
}

void test_array_minus_1_result(int status,
                               MMDB_entry_data_s entry_data,
                               char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - array[-1]",
           function);
    ok(entry_data.has_data, "found a value for array[-1]");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - array[-1]");
    cmp_ok(entry_data.uint32, "==", 3, "entry value is 3 - array[-1]");
}

int call_vget_value(MMDB_entry_s *entry, MMDB_entry_data_s *entry_data, ...) {
    va_list keys;
    va_start(keys, entry_data);

    int status = MMDB_vget_value(entry, entry_data, keys);

    va_end(keys);

    return status;
}

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

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

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {"array", "0", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_array_0_result(status, entry_data, "MMDB_aget_value");

        status = MMDB_get_value(&result.entry, &entry_data, "array", "0", NULL);
        test_array_0_result(status, entry_data, "MMDB_get_value");

        status =
            call_vget_value(&result.entry, &entry_data, "array", "0", NULL);
        test_array_0_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {"array", "2", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_array_2_result(status, entry_data, "MMDB_aget_value");

        status = MMDB_get_value(&result.entry, &entry_data, "array", "2", NULL);
        test_array_2_result(status, entry_data, "MMDB_get_value");

        status =
            call_vget_value(&result.entry, &entry_data, "array", "2", NULL);
        test_array_2_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        int status =
            MMDB_get_value(&result.entry, &entry_data, "array", "zero", NULL);
        cmp_ok(status,
               "==",
               MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR,
               "MMDB_get_value() returns error on non-integer array index");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {"array", "-1", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_array_minus_1_result(status, entry_data, "MMDB_aget_value");

        status =
            MMDB_get_value(&result.entry, &entry_data, "array", "-1", NULL);
        test_array_minus_1_result(status, entry_data, "MMDB_get_value");

        status =
            call_vget_value(&result.entry, &entry_data, "array", "-1", NULL);
        test_array_minus_1_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {"array", "-3", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_array_minus_3_result(status, entry_data, "MMDB_aget_value");

        status =
            MMDB_get_value(&result.entry, &entry_data, "array", "-3", NULL);
        test_array_minus_3_result(status, entry_data, "MMDB_get_value");

        status =
            call_vget_value(&result.entry, &entry_data, "array", "-3", NULL);
        test_array_minus_3_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        int status =
            MMDB_get_value(&result.entry, &entry_data, "array", "-4", NULL);
        cmp_ok(status,
               "==",
               MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR,
               "MMDB_get_value() returns error on too large negative integer");
    }

    {
        MMDB_entry_data_s entry_data;
        int status = MMDB_get_value(
            &result.entry, &entry_data, "array", "-18446744073709551616", NULL);
        cmp_ok(
            status,
            "==",
            MMDB_INVALID_LOOKUP_PATH_ERROR,
            "MMDB_get_value() returns error on integer smaller than LONG_MIN");
    }

    {
        MMDB_entry_data_s entry_data;
        int status = MMDB_get_value(
            &result.entry, &entry_data, "array", "18446744073709551616", NULL);
        cmp_ok(
            status,
            "==",
            MMDB_INVALID_LOOKUP_PATH_ERROR,
            "MMDB_get_value() returns error on integer larger than LONG_MAX");
    }

    MMDB_close(mmdb);
    free(mmdb);
}

void test_complex_map_a_result(int status,
                               MMDB_entry_data_s entry_data,
                               char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - map1{map2}{array}[0]{map3}{a}",
           function);
    ok(entry_data.has_data, "found a value for map1{map2}{array}[0]{map3}{a}");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - map1{map2}{array}[0]{map3}{a}");
    cmp_ok(entry_data.uint32,
           "==",
           1,
           "entry value is 1 - map1{map2}{array}[0]{map3}{a}");
}

void test_complex_map_c_result(int status,
                               MMDB_entry_data_s entry_data,
                               char *function) {
    cmp_ok(status,
           "==",
           MMDB_SUCCESS,
           "status for %s() is MMDB_SUCCESS - map1{map2}{array}[0]{map3}{c}",
           function);
    ok(entry_data.has_data, "found a value for map1{map2}{array}[0]{map3}{c}");
    cmp_ok(entry_data.type,
           "==",
           MMDB_DATA_TYPE_UINT32,
           "returned entry type is uint32 - map1{map2}{array}[0]{map3}{c}");
    cmp_ok(entry_data.uint32,
           "==",
           3,
           "entry value is 3 - map1{map2}{array}[0]{map3}{c}");
}

void test_no_result(int status,
                    MMDB_entry_data_s entry_data,
                    char *function,
                    char *path_description) {
    cmp_ok(status,
           "==",
           MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR,
           "status for %s() is MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR - %s",
           function,
           path_description);
    ok(!entry_data.has_data, "did not find a value for %s", path_description);
}

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

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

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {
            "map1", "map2", "array", "0", "map3", "a", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_complex_map_a_result(status, entry_data, "MMDB_aget_value");

        status = MMDB_get_value(&result.entry,
                                &entry_data,
                                "map1",
                                "map2",
                                "array",
                                "0",
                                "map3",
                                "a",
                                NULL);
        test_complex_map_a_result(status, entry_data, "MMDB_get_value");

        status = call_vget_value(&result.entry,
                                 &entry_data,
                                 "map1",
                                 "map2",
                                 "array",
                                 "0",
                                 "map3",
                                 "a",
                                 NULL);
        test_complex_map_a_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {
            "map1", "map2", "array", "0", "map3", "c", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_complex_map_c_result(status, entry_data, "MMDB_aget_value");

        status = MMDB_get_value(&result.entry,
                                &entry_data,
                                "map1",
                                "map2",
                                "array",
                                "0",
                                "map3",
                                "c",
                                NULL);
        test_complex_map_c_result(status, entry_data, "MMDB_get_value");

        status = call_vget_value(&result.entry,
                                 &entry_data,
                                 "map1",
                                 "map2",
                                 "array",
                                 "0",
                                 "map3",
                                 "c",
                                 NULL);
        test_complex_map_c_result(status, entry_data, "MMDB_vget_value");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {
            "map1", "map42", "array", "0", "map3", "c", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_no_result(status,
                       entry_data,
                       "MMDB_aget_value",
                       "map1{map42}{array}[0]{map3}{c}");

        status = MMDB_get_value(&result.entry,
                                &entry_data,
                                "map1",
                                "map42",
                                "array",
                                "0",
                                "map3",
                                "c",
                                NULL);
        test_no_result(status,
                       entry_data,
                       "MMDB_get_value",
                       "map1{map42}{array}[0]{map3}{c}");

        status = call_vget_value(&result.entry,
                                 &entry_data,
                                 "map1",
                                 "map42",
                                 "array",
                                 "0",
                                 "map3",
                                 "c",
                                 NULL);
        test_no_result(status,
                       entry_data,
                       "MMDB_vget_value",
                       "map1{map42}{array}[0]{map3}{c}");
    }

    {
        MMDB_entry_data_s entry_data;
        const char *lookup_path[] = {
            "map1", "map2", "array", "9", "map3", "c", NULL};
        int status = MMDB_aget_value(&result.entry, &entry_data, lookup_path);
        test_no_result(status,
                       entry_data,
                       "MMDB_aget_value",
                       "map1{map42}{array}[9]{map3}{c}");

        status = MMDB_get_value(&result.entry,
                                &entry_data,
                                "map1",
                                "map2",
                                "array",
                                "9",
                                "map3",
                                "c",
                                NULL);
        test_no_result(status,
                       entry_data,
                       "MMDB_get_value",
                       "map1{map42}{array}[9]{map3}{c}");

        status = call_vget_value(&result.entry,
                                 &entry_data,
                                 "map1",
                                 "map2",
                                 "array",
                                 "9",
                                 "map3",
                                 "c",
                                 NULL);
        test_no_result(status,
                       entry_data,
                       "MMDB_vget_value",
                       "map1{map42}{array}[9]{map3}{c}");
    }

    MMDB_close(mmdb);
    free(mmdb);
}

void run_tests(int mode, const char *mode_desc) {
    test_simple_structure(mode, mode_desc);
    test_nested_structure(mode, mode_desc);
}

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