/*
 * Copyright 2019-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "test-mongocrypt-assert-match-bson.h"
#include "test-mongocrypt-assert.h"

#include <mc-mlib/macros.h>

/* string comparison functions for Windows */
#ifdef _WIN32
#define strcasecmp _stricmp
#define strncasecmp _strnicmp
#endif

/* The following matching logic is copied from libmongoc. */
bool bson_init_from_value(bson_t *b, const bson_value_t *v);

char *single_quotes_to_double(const char *str);

/* match_action_t determines if default check for a field is overridden. */
typedef enum {
    MATCH_ACTION_SKIP,    /* do not use the default check. */
    MATCH_ACTION_ABORT,   /* an error occurred, stop checking. */
    MATCH_ACTION_CONTINUE /* use the default check. */
} match_action_t;

struct _match_ctx_t;
/* doc_iter may be null if the pattern field is not found. */
typedef match_action_t (*match_visitor_fn)(struct _match_ctx_t *ctx, bson_iter_t *pattern_iter, bson_iter_t *doc_iter);

typedef struct _match_ctx_t {
    char errmsg[1000];
    bool strict_numeric_types;
    /* if retain_dots_in_keys is true, then don't consider a path with dots to
     * indicate recursing into a sub document. */
    bool retain_dots_in_keys;
    /* if allow_placeholders is true, treats 42 and "42" as placeholders. I.e.
     * comparing 42 to anything is ok. */
    bool allow_placeholders;
    /* path is the dot separated breadcrumb trail of keys. */
    char path[1000];
    /* if visitor_fn is not NULL, this is called on for every key in the pattern.
     * The returned match_action_t can override the default match behavior. */
    match_visitor_fn visitor_fn;
    void *visitor_ctx;
    /* if is_command is true, then compare the first key case insensitively. */
    bool is_command;
} match_ctx_t;

void assert_match_bson(const bson_t *doc, const bson_t *pattern, bool is_command);

bool match_bson(const bson_t *doc, const bson_t *pattern, bool is_command);

int64_t bson_value_as_int64(const bson_value_t *value);

bool match_bson_value(const bson_value_t *doc, const bson_value_t *pattern, match_ctx_t *ctx);

bool match_bson_with_ctx(const bson_t *doc, const bson_t *pattern, match_ctx_t *ctx);

bool match_json(const bson_t *doc,
                bool is_command,
                const char *filename,
                int lineno,
                const char *funcname,
                const char *json_pattern,
                ...);

const char *_mongoc_bson_type_to_str(bson_type_t t);

static bool get_exists_operator(const bson_value_t *value, bool *exists);

static bool get_empty_operator(const bson_value_t *value, bool *exists);

static bool get_type_operator(const bson_value_t *value, bson_type_t *out);

static bool is_empty_doc_or_array(const bson_value_t *value);

static bool
find(bson_iter_t *iter, const bson_t *doc, const char *key, bool is_command, bool is_first, bool retain_dots_in_keys);

/*--------------------------------------------------------------------------
 *
 * single_quotes_to_double --
 *
 *       Copy str with single-quotes replaced by double.
 *
 * Returns:
 *       A string you must bson_free.
 *
 * Side effects:
 *       None.
 *
 *--------------------------------------------------------------------------
 */

char *single_quotes_to_double(const char *str) {
    char *result = bson_strdup(str);
    char *p;

    for (p = result; *p; p++) {
        if (*p == '\'') {
            *p = '"';
        }
    }

    return result;
}

/*--------------------------------------------------------------------------
 *
 * match_json --
 *
 *       Call match_bson on "doc" and "json_pattern".
 *       For convenience, single-quotes are synonymous with double-quotes.
 *
 *       A NULL doc or NULL json_pattern means "{}".
 *
 * Returns:
 *       True or false.
 *
 * Side effects:
 *       Logs if no match. Aborts if json is malformed.
 *
 *--------------------------------------------------------------------------
 */

MLIB_ANNOTATE_PRINTF(6, 7)

bool match_json(const bson_t *doc,
                bool is_command,
                const char *filename,
                int lineno,
                const char *funcname,
                const char *json_pattern,
                ...) {
    va_list args;
    char *json_pattern_formatted;
    char *double_quoted;
    bson_error_t error;
    bson_t *pattern;
    match_ctx_t ctx = {{0}};
    bool matches;

    va_start(args, json_pattern);
    json_pattern_formatted = bson_strdupv_printf(json_pattern ? json_pattern : "{}", args);
    va_end(args);

    double_quoted = single_quotes_to_double(json_pattern_formatted);
    pattern = bson_new_from_json((const uint8_t *)double_quoted, -1, &error);

    if (!pattern) {
        TEST_STDERR_PRINTF("couldn't parse JSON: %s\n", error.message);
        abort();
    }

    ctx.is_command = is_command;
    matches = match_bson_with_ctx(doc, pattern, &ctx);

    if (!matches) {
        char *as_string = doc ? bson_as_canonical_extended_json(doc, NULL) : NULL;
        TEST_STDERR_PRINTF("ASSERT_MATCH failed with document:\n\n"
                           "%s\n"
                           "pattern:\n%s\n"
                           "%s\n"
                           "%s:%d %s()\n",
                           as_string ? as_string : "{}",
                           double_quoted,
                           ctx.errmsg,
                           filename,
                           lineno,
                           funcname);
        bson_free(as_string);
    }

    bson_destroy(pattern);
    bson_free(json_pattern_formatted);
    bson_free(double_quoted);

    return matches;
}

/*--------------------------------------------------------------------------
 *
 * match_bson --
 *
 *       Does "doc" match "pattern"?
 *
 *       See match_bson_with_ctx for details.
 *
 * Returns:
 *       True or false.
 *
 * Side effects:
 *       None.
 *
 *--------------------------------------------------------------------------
 */

bool match_bson(const bson_t *doc, const bson_t *pattern, bool is_command) {
    match_ctx_t ctx = {{0}};

    ctx.strict_numeric_types = true;
    ctx.is_command = is_command;

    return match_bson_with_ctx(doc, pattern, &ctx);
}

MLIB_ANNOTATE_PRINTF(2, 3)

static void match_err(match_ctx_t *ctx, const char *fmt, ...) {
    va_list args;
    char *formatted;

    BSON_ASSERT(ctx);

    va_start(args, fmt);
    formatted = bson_strdupv_printf(fmt, args);
    va_end(args);

    BSON_ASSERT(0 < bson_snprintf(ctx->errmsg, sizeof ctx->errmsg, "%s: %s", ctx->path, formatted)); // Truncation OK.

    bson_free(formatted);
}

/* When matching two docs, and preparing to recurse to match two subdocs with
 * the given key, derive context for matching them from the current context. */
static void derive(match_ctx_t *ctx, match_ctx_t *derived, const char *key) {
    BSON_ASSERT(ctx);
    BSON_ASSERT(derived);
    BSON_ASSERT(key);

    derived->strict_numeric_types = ctx->strict_numeric_types;

    if (strlen(ctx->path) > 0) {
        BSON_ASSERT(0 < bson_snprintf(derived->path, sizeof derived->path, "%s.%s", ctx->path, key)); // Truncation OK.
    } else {
        BSON_ASSERT(0 < bson_snprintf(derived->path, sizeof derived->path, "%s", key)); // Truncation OK.
    }
    derived->retain_dots_in_keys = ctx->retain_dots_in_keys;
    derived->allow_placeholders = ctx->allow_placeholders;
    derived->visitor_ctx = ctx->visitor_ctx;
    derived->visitor_fn = ctx->visitor_fn;
    derived->is_command = false;
    derived->errmsg[0] = 0;
}

/*--------------------------------------------------------------------------
 *
 * match_bson_with_ctx --
 *
 *       Does "doc" match "pattern"?
 *
 *       mongoc_matcher_t prohibits $-prefixed keys, which is something
 *       we need to test in e.g. test_mongoc_client_read_prefs, so this
 *       does *not* use mongoc_matcher_t. Instead, "doc" matches "pattern"
 *       if its key-value pairs are a simple superset of pattern's. Order
 *       matters.
 *
 *       The only special pattern syntaxes are:
 *         "field": {"$exists": true/false}
 *         "field": {"$empty": true/false}
 *         "field": {"$$type": "type string"}
 *
 *       The first key matches case-insensitively if ctx->is_command.
 *
 *       An optional match visitor (match_visitor_fn and match_visitor_ctx)
 *       can be set in ctx to provide custom matching behavior.
 *
 *       A NULL doc or NULL pattern means "{}".
 *
 * Returns:
 *       True or false.
 *
 * Side effects:
 *       None.
 *
 *--------------------------------------------------------------------------
 */

bool match_bson_with_ctx(const bson_t *doc, const bson_t *pattern, match_ctx_t *ctx) {
    bson_iter_t pattern_iter;
    const char *key;
    const bson_value_t *value;
    bool is_first = true;
    bool is_exists_operator;
    bool is_empty_operator;
    bool is_type_operator;
    bool exists;
    bool empty = false;
    bson_type_t bson_type = (bson_type_t)0;
    bool found;
    bson_iter_t doc_iter;
    bson_value_t doc_value;
    match_ctx_t derived;

    if (bson_empty0(pattern)) {
        /* matches anything */
        return true;
    }

    BSON_ASSERT(bson_iter_init(&pattern_iter, pattern));

    while (bson_iter_next(&pattern_iter)) {
        key = bson_iter_key(&pattern_iter);
        value = bson_iter_value(&pattern_iter);

        found = find(&doc_iter, doc, key, ctx->is_command, is_first, ctx->retain_dots_in_keys);
        if (found) {
            bson_value_copy(bson_iter_value(&doc_iter), &doc_value);
        }

        /* is value {"$exists": true} or {"$exists": false} ? */
        is_exists_operator = get_exists_operator(value, &exists);

        /* is value {"$empty": true} or {"$empty": false} ? */
        is_empty_operator = get_empty_operator(value, &empty);

        /* is value {"$$type": "string" } ? */
        is_type_operator = get_type_operator(value, &bson_type);

        derive(ctx, &derived, key);

        if (ctx->visitor_fn) {
            match_action_t action = ctx->visitor_fn(ctx, &pattern_iter, found ? &doc_iter : NULL);
            if (action == MATCH_ACTION_ABORT) {
                goto fail;
            } else if (action == MATCH_ACTION_SKIP) {
                goto next;
            }
        }

        if (value->value_type == BSON_TYPE_NULL && found) {
            /* pattern has "key": null, and "key" is in doc */
            if (doc_value.value_type != BSON_TYPE_NULL) {
                match_err(&derived, "%s should be null or absent", key);
                goto fail;
            }
        } else if (is_exists_operator) {
            if (exists != found) {
                match_err(&derived, "%s found", found ? "" : "not");
                goto fail;
            }
        } else if (!found) {
            match_err(&derived, "key '%s' not found", key);
            goto fail;
        } else if (is_empty_operator) {
            if (empty != is_empty_doc_or_array(&doc_value)) {
                match_err(&derived, "%s found", empty ? "" : " not");
                goto fail;
            }
        } else if (is_type_operator) {
            if (doc_value.value_type != bson_type) {
                match_err(&derived, "incorrect type");
                goto fail;
            }
        } else if (!match_bson_value(&doc_value, value, &derived)) {
            goto fail;
        }

    next:
        is_first = false;
        if (found) {
            bson_value_destroy(&doc_value);
        }
    }

    return true;

fail:
    if (found) {
        bson_value_destroy(&doc_value);
    }

    if (strlen(derived.errmsg) > 0) {
        memcpy(ctx->errmsg, derived.errmsg, sizeof(derived.errmsg));
    }

    return false;
}

/*--------------------------------------------------------------------------
 *
 * find --
 *
 *       Find the value for a key.
 *
 * Returns:
 *       Whether the key was found.
 *
 * Side effects:
 *       Copies the found value into "iter_out".
 *
 *--------------------------------------------------------------------------
 */

static bool find(bson_iter_t *iter_out,
                 const bson_t *doc,
                 const char *key,
                 bool is_command,
                 bool is_first,
                 bool retain_dots_in_keys) {
    bson_iter_t iter;
    bson_iter_t descendent;

    bson_iter_init(&iter, doc);

    if (!retain_dots_in_keys && strchr(key, '.')) {
        if (!bson_iter_find_descendant(&iter, key, &descendent)) {
            return false;
        }

        memcpy(iter_out, &descendent, sizeof(bson_iter_t));
        return true;
    } else if (is_command && is_first) {
        if (!bson_iter_find_case(&iter, key)) {
            return false;
        }
    } else if (!bson_iter_find(&iter, key)) {
        return false;
    }

    memcpy(iter_out, &iter, sizeof(bson_iter_t));
    return true;
}

bool bson_init_from_value(bson_t *b, const bson_value_t *v) {
    BSON_ASSERT(v->value_type == BSON_TYPE_ARRAY || v->value_type == BSON_TYPE_DOCUMENT);

    return bson_init_static(b, v->value.v_doc.data, v->value.v_doc.data_len);
}

static bool _is_operator(const char *op_name, const bson_value_t *value, bool *op_val) {
    bson_t bson;
    bson_iter_t iter;

    if (value->value_type == BSON_TYPE_DOCUMENT && bson_init_from_value(&bson, value)
        && bson_iter_init_find(&iter, &bson, op_name)) {
        *op_val = bson_iter_as_bool(&iter);
        return true;
    }

    return false;
}

/*--------------------------------------------------------------------------
 *
 * get_exists_operator --
 *
 *       Is value a subdocument like {"$exists": bool}?
 *
 * Returns:
 *       True if the value is a subdocument with the first key "$exists",
 *       or if value is BSON null.
 *
 * Side effects:
 *       If the function returns true, *exists is set to true or false,
 *       the value of the bool.
 *
 *--------------------------------------------------------------------------
 */

static bool get_exists_operator(const bson_value_t *value, bool *exists) {
    if (_is_operator("$exists", value, exists)) {
        return true;
    }

    if (value->value_type == BSON_TYPE_NULL) {
        *exists = false;
        return true;
    }

    return false;
}

/*--------------------------------------------------------------------------
 *
 * get_empty_operator --
 *
 *       Is value a subdocument like {"$empty": bool}?
 *
 * Returns:
 *       True if the value is a subdocument with the first key "$empty".
 *
 * Side effects:
 *       If the function returns true, *empty is set to true or false,
 *       the value of the bool.
 *
 *--------------------------------------------------------------------------
 */

bool get_empty_operator(const bson_value_t *value, bool *empty) {
    return _is_operator("$empty", value, empty);
}

/*--------------------------------------------------------------------------
 *
 * get_type_operator --
 *
 *       Is value a subdocument like {"$$type": "BSON type string"}?
 *
 * Returns:
 *       True if the value is a subdocument with the first key "$$type",
 *       and sets the @bson_type.
 *
 * Side effects:
 *       If the function returns true, *@bson_type is set.
 *
 *--------------------------------------------------------------------------
 */

static bool get_type_operator(const bson_value_t *value, bson_type_t *out) {
    bson_t bson;
    bson_iter_t iter;
    const char *value_string;

    /* See list of aliases on this page:
     * https://docs.mongodb.com/manual/reference/bson-types/ */
    if (value->value_type == BSON_TYPE_DOCUMENT && bson_init_from_value(&bson, value)
        && bson_iter_init_find(&iter, &bson, "$$type")) {
        value_string = bson_iter_utf8(&iter, NULL);
        if (0 == strcasecmp("double", value_string)) {
            *out = BSON_TYPE_DOUBLE;
        } else if (0 == strcasecmp("string", value_string)) {
            *out = BSON_TYPE_UTF8;
        } else if (0 == strcasecmp("object", value_string)) {
            *out = BSON_TYPE_DOCUMENT;
        } else if (0 == strcasecmp("array", value_string)) {
            *out = BSON_TYPE_ARRAY;
        } else if (0 == strcasecmp("binData", value_string)) {
            *out = BSON_TYPE_BINARY;
        } else if (0 == strcasecmp("undefined", value_string)) {
            *out = BSON_TYPE_UNDEFINED;
        } else if (0 == strcasecmp("objectId", value_string)) {
            *out = BSON_TYPE_OID;
        } else if (0 == strcasecmp("bool", value_string)) {
            *out = BSON_TYPE_BOOL;
        } else if (0 == strcasecmp("date", value_string)) {
            *out = BSON_TYPE_DATE_TIME;
        } else if (0 == strcasecmp("null", value_string)) {
            *out = BSON_TYPE_NULL;
        } else if (0 == strcasecmp("regex", value_string)) {
            *out = BSON_TYPE_REGEX;
        } else if (0 == strcasecmp("dbPointer", value_string)) {
            *out = BSON_TYPE_DBPOINTER;
        } else if (0 == strcasecmp("javascript", value_string)) {
            *out = BSON_TYPE_CODE;
        } else if (0 == strcasecmp("symbol", value_string)) {
            *out = BSON_TYPE_SYMBOL;
        } else if (0 == strcasecmp("javascriptWithScope", value_string)) {
            *out = BSON_TYPE_CODEWSCOPE;
        } else if (0 == strcasecmp("int", value_string)) {
            *out = BSON_TYPE_INT32;
        } else if (0 == strcasecmp("timestamp", value_string)) {
            *out = BSON_TYPE_TIMESTAMP;
        } else if (0 == strcasecmp("long", value_string)) {
            *out = BSON_TYPE_INT64;
        } else if (0 == strcasecmp("decimal", value_string)) {
            *out = BSON_TYPE_DECIMAL128;
        } else if (0 == strcasecmp("minKey", value_string)) {
            *out = BSON_TYPE_MINKEY;
        } else if (0 == strcasecmp("maxKey", value_string)) {
            *out = BSON_TYPE_MAXKEY;
        } else {
            TEST_STDERR_PRINTF("unrecognized $$type value: %s\n", value_string);
            abort();
        }
        return true;
    }

    return false;
}

/*--------------------------------------------------------------------------
 *
 * is_empty_doc_or_array --
 *
 *       Is value the subdocument {} or the array []?
 *
 *--------------------------------------------------------------------------
 */

static bool is_empty_doc_or_array(const bson_value_t *value) {
    bson_t doc;

    if (!(value->value_type == BSON_TYPE_ARRAY || value->value_type == BSON_TYPE_DOCUMENT)) {
        return false;
    }
    BSON_ASSERT(bson_init_static(&doc, value->value.v_doc.data, value->value.v_doc.data_len));

    return bson_count_keys(&doc) == 0;
}

static bool match_bson_arrays(const bson_t *array, const bson_t *pattern, match_ctx_t *ctx) {
    uint32_t array_count;
    uint32_t pattern_count;
    bson_iter_t array_iter;
    bson_iter_t pattern_iter;
    const bson_value_t *array_value;
    const bson_value_t *pattern_value;
    match_ctx_t derived;

    array_count = bson_count_keys(array);
    pattern_count = bson_count_keys(pattern);

    if (array_count != pattern_count) {
        match_err(ctx, "expected %" PRIu32 " keys, not %" PRIu32, pattern_count, array_count);
        return false;
    }

    BSON_ASSERT(bson_iter_init(&array_iter, array));
    BSON_ASSERT(bson_iter_init(&pattern_iter, pattern));

    while (bson_iter_next(&array_iter)) {
        BSON_ASSERT(bson_iter_next(&pattern_iter));
        array_value = bson_iter_value(&array_iter);
        pattern_value = bson_iter_value(&pattern_iter);

        derive(ctx, &derived, bson_iter_key(&array_iter));

        if (!match_bson_value(array_value, pattern_value, &derived)) {
            // Propagate error message.
            if (strlen(derived.errmsg) > 0) {
                memcpy(ctx->errmsg, derived.errmsg, sizeof(derived.errmsg));
            }
            return false;
        }
    }

    return true;
}

static bool is_number_type(bson_type_t t) {
    if (t == BSON_TYPE_DOUBLE || t == BSON_TYPE_INT32 || t == BSON_TYPE_INT64) {
        return true;
    }

    return false;
}

int64_t bson_value_as_int64(const bson_value_t *value) {
    if (value->value_type == BSON_TYPE_DOUBLE) {
        return (int64_t)value->value.v_double;
    } else if (value->value_type == BSON_TYPE_INT32) {
        return (int64_t)value->value.v_int32;
    } else if (value->value_type == BSON_TYPE_INT64) {
        return value->value.v_int64;
    } else {
        return -123;
    }
}

bool match_bson_value(const bson_value_t *doc, const bson_value_t *pattern, match_ctx_t *ctx) {
    bson_t subdoc;
    bson_t pattern_subdoc;
    int64_t doc_int64;
    int64_t pattern_int64;
    bool ret = false;

    if (ctx && ctx->allow_placeholders) {
        /* The change streams spec tests use the value 42 as a placeholder. */
        bool is_placeholder = false;
        if (is_number_type(pattern->value_type) && bson_value_as_int64(pattern) == 42) {
            is_placeholder = true;
        }
        if (pattern->value_type == BSON_TYPE_UTF8 && !strcmp(pattern->value.v_utf8.str, "42")) {
            is_placeholder = true;
        }
        if (is_placeholder) {
            return true;
        }
    }

    if (is_number_type(doc->value_type) && is_number_type(pattern->value_type) && ctx && !ctx->strict_numeric_types) {
        doc_int64 = bson_value_as_int64(doc);
        pattern_int64 = bson_value_as_int64(pattern);

        if (doc_int64 != pattern_int64) {
            match_err(ctx, "expected %" PRId64 ", got %" PRId64, pattern_int64, doc_int64);
            return false;
        }

        return true;
    }

    if (doc->value_type != pattern->value_type) {
        match_err(ctx,
                  "expected type %s, got %s",
                  _mongoc_bson_type_to_str(pattern->value_type),
                  _mongoc_bson_type_to_str(doc->value_type));
        return false;
    }

    switch (doc->value_type) {
    case BSON_TYPE_ARRAY:
    case BSON_TYPE_DOCUMENT:

        if (!bson_init_from_value(&subdoc, doc)) {
            return false;
        }

        if (!bson_init_from_value(&pattern_subdoc, pattern)) {
            bson_destroy(&subdoc);
            return false;
        }

        if (doc->value_type == BSON_TYPE_ARRAY) {
            ret = match_bson_arrays(&subdoc, &pattern_subdoc, ctx);
        } else {
            ret = match_bson_with_ctx(&subdoc, &pattern_subdoc, ctx);
        }

        bson_destroy(&subdoc);
        bson_destroy(&pattern_subdoc);

        return ret;

    case BSON_TYPE_BINARY:
        ret = doc->value.v_binary.data_len == pattern->value.v_binary.data_len
           && !memcmp(doc->value.v_binary.data, pattern->value.v_binary.data, doc->value.v_binary.data_len);
        break;

    case BSON_TYPE_BOOL:
        ret = doc->value.v_bool == pattern->value.v_bool;

        if (!ret) {
            match_err(ctx, "expected %d, got %d", pattern->value.v_bool, doc->value.v_bool);
        }

        return ret;

    case BSON_TYPE_CODE:
        ret = doc->value.v_code.code_len == pattern->value.v_code.code_len
           && !memcmp(doc->value.v_code.code, pattern->value.v_code.code, doc->value.v_code.code_len);

        break;

    case BSON_TYPE_CODEWSCOPE:
        ret = doc->value.v_codewscope.code_len == pattern->value.v_codewscope.code_len
           && !memcmp(doc->value.v_codewscope.code, pattern->value.v_codewscope.code, doc->value.v_codewscope.code_len)
           && doc->value.v_codewscope.scope_len == pattern->value.v_codewscope.scope_len
           && !memcmp(doc->value.v_codewscope.scope_data,
                      pattern->value.v_codewscope.scope_data,
                      doc->value.v_codewscope.scope_len);

        break;

    case BSON_TYPE_DATE_TIME:
        ret = doc->value.v_datetime == pattern->value.v_datetime;

        if (!ret) {
            match_err(ctx, "expected %" PRId64 ", got %" PRId64, pattern->value.v_datetime, doc->value.v_datetime);
        }

        return ret;

    case BSON_TYPE_DOUBLE:
        ret = doc->value.v_double == pattern->value.v_double;

        if (!ret) {
            match_err(ctx, "expected %f, got %f", pattern->value.v_double, doc->value.v_double);
        }

        return ret;

    case BSON_TYPE_INT32:
        ret = doc->value.v_int32 == pattern->value.v_int32;

        if (!ret) {
            match_err(ctx, "expected %" PRId32 ", got %" PRId32, pattern->value.v_int32, doc->value.v_int32);
        }

        return ret;

    case BSON_TYPE_INT64:
        ret = doc->value.v_int64 == pattern->value.v_int64;

        if (!ret) {
            match_err(ctx, "expected %" PRId64 ", got %" PRId64, pattern->value.v_int64, doc->value.v_int64);
        }

        return ret;

    case BSON_TYPE_OID: ret = bson_oid_equal(&doc->value.v_oid, &pattern->value.v_oid); break;

    case BSON_TYPE_REGEX:
        ret = !strcmp(doc->value.v_regex.regex, pattern->value.v_regex.regex)
           && !strcmp(doc->value.v_regex.options, pattern->value.v_regex.options);

        break;

    case BSON_TYPE_SYMBOL:
        ret = doc->value.v_symbol.len == pattern->value.v_symbol.len
           && !strncmp(doc->value.v_symbol.symbol, pattern->value.v_symbol.symbol, doc->value.v_symbol.len);

        break;

    case BSON_TYPE_TIMESTAMP:
        ret = doc->value.v_timestamp.timestamp == pattern->value.v_timestamp.timestamp
           && doc->value.v_timestamp.increment == pattern->value.v_timestamp.increment;

        break;

    case BSON_TYPE_UTF8:
        ret = doc->value.v_utf8.len == pattern->value.v_utf8.len
           && !strncmp(doc->value.v_utf8.str, pattern->value.v_utf8.str, doc->value.v_utf8.len);

        if (!ret) {
            match_err(ctx, "expected \"%s\", got \"%s\"", pattern->value.v_utf8.str, doc->value.v_utf8.str);
        }

        return ret;

    /* these are empty types, if "a" and "b" are the same type they're equal */
    case BSON_TYPE_EOD:
    case BSON_TYPE_MAXKEY:
    case BSON_TYPE_MINKEY:
    case BSON_TYPE_NULL:
    case BSON_TYPE_UNDEFINED: return true;

    case BSON_TYPE_DBPOINTER:
        ret = (0 == strcmp(doc->value.v_dbpointer.collection, pattern->value.v_dbpointer.collection)
               && bson_oid_equal(&doc->value.v_dbpointer.oid, &pattern->value.v_dbpointer.oid));
        break;

    case BSON_TYPE_DECIMAL128:
        ret = (doc->value.v_decimal128.low == pattern->value.v_decimal128.low
               && doc->value.v_decimal128.high == pattern->value.v_decimal128.high);
        if (!ret) {
            match_err(ctx,
                      "Decimal128 is not an exact binary match (though "
                      "numeric values may be equal)");
        }
        break;
    default:
        match_err(ctx, "unexpected value type %d: %s", (int)doc->value_type, _mongoc_bson_type_to_str(doc->value_type));
    }

    if (!ret) {
        match_err(ctx, "%s values mismatch", _mongoc_bson_type_to_str(pattern->value_type));
    }

    return ret;
}

const char *_mongoc_bson_type_to_str(bson_type_t t) {
    switch (t) {
    case BSON_TYPE_EOD: return "EOD";
    case BSON_TYPE_DOUBLE: return "DOUBLE";
    case BSON_TYPE_UTF8: return "UTF8";
    case BSON_TYPE_DOCUMENT: return "DOCUMENT";
    case BSON_TYPE_ARRAY: return "ARRAY";
    case BSON_TYPE_BINARY: return "BINARY";
    case BSON_TYPE_UNDEFINED: return "UNDEFINED";
    case BSON_TYPE_OID: return "OID";
    case BSON_TYPE_BOOL: return "BOOL";
    case BSON_TYPE_DATE_TIME: return "DATE_TIME";
    case BSON_TYPE_NULL: return "NULL";
    case BSON_TYPE_REGEX: return "REGEX";
    case BSON_TYPE_DBPOINTER: return "DBPOINTER";
    case BSON_TYPE_CODE: return "CODE";
    case BSON_TYPE_SYMBOL: return "SYMBOL";
    case BSON_TYPE_CODEWSCOPE: return "CODEWSCOPE";
    case BSON_TYPE_INT32: return "INT32";
    case BSON_TYPE_TIMESTAMP: return "TIMESTAMP";
    case BSON_TYPE_INT64: return "INT64";
    case BSON_TYPE_MAXKEY: return "MAXKEY";
    case BSON_TYPE_MINKEY: return "MINKEY";
    case BSON_TYPE_DECIMAL128: return "DECIMAL128";
    default: return "Unknown";
    }
}

bool _check_match_bson(const bson_t *doc, const bson_t *pattern, char *errmsg, size_t errmsg_len) {
    // Set `retain_dots_in_keys` to interpret a pattern key "db.test" as a key, rather than a key path.
    match_ctx_t ctx = {.retain_dots_in_keys = true};
    bool matched = match_bson_with_ctx(doc, pattern, &ctx);
    if (matched) {
        bson_strncpy(errmsg, "", errmsg_len);
    } else {
        if (matched) {
            bson_strncpy(errmsg, ctx.errmsg, errmsg_len);
        }
    }
    return matched;
}
