// Copyright 2009-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.

#pragma once

#include <mongocxx/exception/exception.hpp>

#include <mongocxx/private/mongoc.hh>
#include <mongocxx/private/ssl.hh>

#include <bsoncxx/test/catch.hh>

#include <catch2/catch_case_sensitive.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>

namespace mongocxx {
namespace test_util {

// Check that an error message includes a substring, case-insensitively. Use like:
// REQUIRE_THROWS_MATCHES(function(), mongocxx::exception, mongocxx_exception_matcher("substring")
class mongocxx_exception_matcher : public Catch::Matchers::MatcherBase<mongocxx::exception> {
    std::string expected_msg;

   public:
    ~mongocxx_exception_matcher() override;

    mongocxx_exception_matcher(mongocxx_exception_matcher&&) = default;
    mongocxx_exception_matcher& operator=(mongocxx_exception_matcher&&) = delete;
    mongocxx_exception_matcher(mongocxx_exception_matcher const&) = default;
    mongocxx_exception_matcher& operator=(mongocxx_exception_matcher const&) = delete;

    mongocxx_exception_matcher(std::string msg) : expected_msg(msg) {}

    bool match(mongocxx::exception const& exc) const override {
        return Catch::Matchers::ContainsSubstring(expected_msg, Catch::CaseSensitive::No).match(exc.what());
    }

    std::string describe() const override {
        return std::string("mongocxx::exception was expected to contain the message: \"") + expected_msg + "\"";
    }
};

} // namespace test_util
} // namespace mongocxx

#define CHECK_OPTIONAL_ARGUMENT(OBJECT, NAME, VALUE) \
    SECTION("has NAME disengaged") {                 \
        REQUIRE(!OBJECT.NAME());                     \
    }                                                \
                                                     \
    SECTION("has a method to set the NAME") {        \
        OBJECT.NAME(VALUE);                          \
        REQUIRE(OBJECT.NAME().value() == VALUE);     \
    }                                                \
    ((void)0)

#define MOCK_POOL_NOSSL                                                                        \
    auto client_pool_new_with_error = libmongoc::client_pool_new_with_error.create_instance(); \
    auto client_pool_destroy = libmongoc::client_pool_destroy.create_instance();               \
    auto client_pool_pop = libmongoc::client_pool_pop.create_instance();                       \
    auto client_pool_push = libmongoc::client_pool_push.create_instance();                     \
    auto client_pool_try_pop = libmongoc::client_pool_try_pop.create_instance();               \
    ((void)0)

#if MONGOCXX_SSL_IS_ENABLED()
#define MOCK_POOL                                                                                   \
    MOCK_POOL_NOSSL;                                                                                \
    auto client_pool_set_ssl_opts = libmongoc::client_pool_set_ssl_opts.create_instance();          \
    client_pool_set_ssl_opts->interpose([](::mongoc_client_pool_t*, const ::mongoc_ssl_opt_t*) {}); \
    ((void)0)
#else
#define MOCK_POOL MOCK_POOL_NOSSL
#endif

#define MOCK_CLIENT_NOSSL                                                                                          \
    auto client_new = libmongoc::client_new_from_uri.create_instance();                                            \
    auto client_destroy = libmongoc::client_destroy.create_instance();                                             \
    auto client_set_read_concern = libmongoc::client_set_read_concern.create_instance();                           \
    auto client_set_preference = libmongoc::client_set_read_prefs.create_instance();                               \
    client_set_preference->interpose([](mongoc_client_t*, mongoc_read_prefs_t const*) {}).forever();               \
    auto client_get_preference = libmongoc::client_get_read_prefs.create_instance();                               \
    client_get_preference->interpose([](mongoc_client_t const*) { return nullptr; }).forever();                    \
    auto client_set_concern = libmongoc::client_set_write_concern.create_instance();                               \
    client_set_concern->interpose([](mongoc_client_t*, mongoc_write_concern_t const*) {}).forever();               \
    auto client_reset = libmongoc::client_reset.create_instance();                                                 \
    client_reset->interpose([](mongoc_client_t const*) {}).forever();                                              \
    auto client_get_concern = libmongoc::client_get_write_concern.create_instance();                               \
    client_get_concern->interpose([](mongoc_client_t const*) { return nullptr; }).forever();                       \
    auto client_start_session = libmongoc::client_start_session.create_instance();                                 \
    auto client_find_databases_with_opts = mongocxx::libmongoc::client_find_databases_with_opts.create_instance(); \
    ((void)0)

#if MONGOCXX_SSL_IS_ENABLED()
#define MOCK_CLIENT                                                                       \
    MOCK_CLIENT_NOSSL;                                                                    \
    auto client_set_ssl_opts = libmongoc::client_set_ssl_opts.create_instance();          \
    client_set_ssl_opts->interpose([](::mongoc_client_t*, const ::mongoc_ssl_opt_t*) {}); \
    ((void)0)
#else
#define MOCK_CLIENT MOCK_CLIENT_NOSSL
#endif

#define MOCK_DATABASE                                                                                                  \
    auto get_database = libmongoc::client_get_database.create_instance();                                              \
    get_database->interpose([&](mongoc_client_t*, char const*) { return nullptr; });                                   \
    auto database_set_read_concern = libmongoc::database_set_read_concern.create_instance();                           \
    auto database_set_preference = libmongoc::database_set_read_prefs.create_instance();                               \
    database_set_preference->interpose([](mongoc_database_t*, mongoc_read_prefs_t const*) {}).forever();               \
    auto database_get_preference = libmongoc::database_get_read_prefs.create_instance();                               \
    database_get_preference->interpose([](mongoc_database_t const*) { return nullptr; }).forever();                    \
    auto database_set_concern = libmongoc::database_set_write_concern.create_instance();                               \
    database_set_concern->interpose([](mongoc_database_t*, mongoc_write_concern_t const*) {}).forever();               \
    auto database_get_concern = libmongoc::database_get_write_concern.create_instance();                               \
    database_get_concern->interpose([](mongoc_database_t const*) { return nullptr; }).forever();                       \
    auto database_destroy = libmongoc::database_destroy.create_instance();                                             \
    database_destroy->interpose([](mongoc_database_t*) {}).forever();                                                  \
    auto database_drop = libmongoc::database_drop.create_instance();                                                   \
    database_drop->interpose([](mongoc_database_t*, bson_error_t*) { return true; }).forever();                        \
    auto database_get_collection = libmongoc::database_get_collection.create_instance();                               \
    database_get_collection->interpose([](mongoc_database_t*, char const*) { return nullptr; }).forever();             \
    auto database_has_collection = libmongoc::database_has_collection.create_instance();                               \
    auto database_command_with_opts = libmongoc::database_command_with_opts.create_instance();                         \
    database_command_with_opts                                                                                         \
        ->interpose(                                                                                                   \
            [](mongoc_database_t*, bson_t const*, mongoc_read_prefs_t const*, bson_t const*, bson_t*, bson_error_t*) { \
                return true;                                                                                           \
            })                                                                                                         \
        .forever();                                                                                                    \
    ((void)0)

#define MOCK_COLLECTION                                                                                            \
    auto collection_set_preference = libmongoc::collection_set_read_prefs.create_instance();                       \
    collection_set_preference->interpose([](mongoc_collection_t*, mongoc_read_prefs_t const*) {}).forever();       \
    auto collection_get_preference = libmongoc::collection_get_read_prefs.create_instance();                       \
    collection_get_preference->interpose([](mongoc_collection_t const*) { return nullptr; }).forever();            \
    auto collection_set_read_concern = libmongoc::collection_set_read_concern.create_instance();                   \
    auto collection_set_concern = libmongoc::collection_set_write_concern.create_instance();                       \
    collection_set_concern->interpose([](mongoc_collection_t*, mongoc_write_concern_t const*) {}).forever();       \
    auto collection_destroy = libmongoc::collection_destroy.create_instance();                                     \
    collection_destroy->interpose([](mongoc_collection_t*) {});                                                    \
    auto collection_drop = libmongoc::collection_drop.create_instance();                                           \
    auto collection_count_documents = libmongoc::collection_count_documents.create_instance();                     \
    auto collection_estimated_document_count = libmongoc::collection_estimated_document_count.create_instance();   \
    auto collection_find_with_opts = libmongoc::collection_find_with_opts.create_instance();                       \
    auto collection_aggregate = libmongoc::collection_aggregate.create_instance();                                 \
    auto collection_get_name = libmongoc::collection_get_name.create_instance();                                   \
    collection_get_name->interpose([](mongoc_collection_t*) { return "dummy_collection"; });                       \
    auto collection_rename = libmongoc::collection_rename.create_instance();                                       \
    auto collection_find_and_modify_with_opts = libmongoc::collection_find_and_modify_with_opts.create_instance(); \
    ((void)0)

#define MOCK_CHANGE_STREAM                                                                         \
    auto collection_watch = libmongoc::collection_watch.create_instance();                         \
    auto database_watch = libmongoc::database_watch.create_instance();                             \
    auto client_watch = libmongoc::client_watch.create_instance();                                 \
    auto change_stream_destroy = libmongoc::change_stream_destroy.create_instance();               \
    auto change_stream_next = libmongoc::change_stream_next.create_instance();                     \
    auto change_stream_error_document = libmongoc::change_stream_error_document.create_instance(); \
    ((void)0)

#define MOCK_FAM                                                                                             \
    auto find_and_modify_opts_destroy = libmongoc::find_and_modify_opts_destroy.create_instance();           \
    find_and_modify_opts_destroy->interpose([](mongoc_find_and_modify_opts_t*) {}).forever();                \
    auto find_and_modify_opts_new = libmongoc::find_and_modify_opts_new.create_instance();                   \
    find_and_modify_opts_new->interpose([]() { return nullptr; }).forever();                                 \
    bool expected_find_and_modify_opts_bypass_document_validation;                                           \
    auto find_and_modify_opts_set_bypass_document_validation =                                               \
        libmongoc::find_and_modify_opts_set_bypass_document_validation.create_instance();                    \
    find_and_modify_opts_set_bypass_document_validation->interpose(                                          \
        [&expected_find_and_modify_opts_bypass_document_validation](                                         \
            mongoc_find_and_modify_opts_t*, bool bypass_document_validation) {                               \
            REQUIRE(bypass_document_validation == expected_find_and_modify_opts_bypass_document_validation); \
            return true;                                                                                     \
        });                                                                                                  \
    bsoncxx::document::view expected_find_and_modify_opts_fields;                                            \
    auto find_and_modify_opts_set_fields = libmongoc::find_and_modify_opts_set_fields.create_instance();     \
    find_and_modify_opts_set_fields->interpose(                                                              \
        [&expected_find_and_modify_opts_fields](mongoc_find_and_modify_opts_t*, const ::bson_t* fields) {    \
            auto fields_view = bsoncxx::helpers::view_from_bson_t(fields);                                   \
            REQUIRE(fields_view == expected_find_and_modify_opts_fields);                                    \
            return true;                                                                                     \
        });                                                                                                  \
    ::mongoc_find_and_modify_flags_t expected_find_and_modify_opts_flags;                                    \
    auto find_and_modify_opts_set_flags = libmongoc::find_and_modify_opts_set_flags.create_instance();       \
    find_and_modify_opts_set_flags->interpose(                                                               \
        [&expected_find_and_modify_opts_flags](                                                              \
            mongoc_find_and_modify_opts_t*, const ::mongoc_find_and_modify_flags_t flags) {                  \
            REQUIRE(flags == expected_find_and_modify_opts_flags);                                           \
            return true;                                                                                     \
        });                                                                                                  \
    bsoncxx::document::view expected_find_and_modify_opts_sort;                                              \
    auto find_and_modify_opts_set_sort = libmongoc::find_and_modify_opts_set_sort.create_instance();         \
    find_and_modify_opts_set_sort->interpose(                                                                \
        [&expected_find_and_modify_opts_sort](mongoc_find_and_modify_opts_t*, const ::bson_t* sort) {        \
            auto sort_view = bsoncxx::helpers::view_from_bson_t(sort);                                       \
            REQUIRE(sort_view == expected_find_and_modify_opts_sort);                                        \
            return true;                                                                                     \
        });                                                                                                  \
    bsoncxx::document::view expected_find_and_modify_opts_update;                                            \
    auto find_and_modify_opts_set_update = libmongoc::find_and_modify_opts_set_update.create_instance();     \
    find_and_modify_opts_set_update->interpose(                                                              \
        [&expected_find_and_modify_opts_update](mongoc_find_and_modify_opts_t*, const ::bson_t* update) {    \
            auto update_view = bsoncxx::helpers::view_from_bson_t(update);                                   \
            REQUIRE(update_view == expected_find_and_modify_opts_update);                                    \
            return true;                                                                                     \
        });                                                                                                  \
    ((void)0)

#define MOCK_CURSOR                                                    \
    auto cursor_destroy = libmongoc::cursor_destroy.create_instance(); \
    cursor_destroy->interpose([&](mongoc_cursor_t*) {});               \
    ((void)0)

#define MOCK_BULK                                                                                                  \
    auto bulk_operation_insert_with_opts = libmongoc::bulk_operation_insert_with_opts.create_instance();           \
    auto bulk_operation_remove_one_with_opts = libmongoc::bulk_operation_remove_one_with_opts.create_instance();   \
    auto bulk_operation_update_one_with_opts = libmongoc::bulk_operation_update_one_with_opts.create_instance();   \
    auto bulk_operation_replace_one_with_opts = libmongoc::bulk_operation_replace_one_with_opts.create_instance(); \
    auto bulk_operation_update_many_with_opts = libmongoc::bulk_operation_update_many_with_opts.create_instance(); \
    auto bulk_operation_remove_many_with_opts = libmongoc::bulk_operation_remove_many_with_opts.create_instance(); \
    auto bulk_operation_set_bypass_document_validation =                                                           \
        libmongoc::bulk_operation_set_bypass_document_validation.create_instance();                                \
    auto bulk_operation_execute = libmongoc::bulk_operation_execute.create_instance();                             \
    auto bulk_operation_destroy = libmongoc::bulk_operation_destroy.create_instance();                             \
    auto collection_create_bulk_operation_with_opts =                                                              \
        libmongoc::collection_create_bulk_operation_with_opts.create_instance();                                   \
    bool bulk_operation_op_called = false;                                                                         \
    bool bulk_operation_set_bypass_document_validation_called = false;                                             \
    bool bulk_operation_execute_called = false;                                                                    \
    bool bulk_operation_destroy_called = false;                                                                    \
    bool collection_create_bulk_operation_called = false;                                                          \
    ((void)0)

#define MOCK_CONCERN                                                     \
    auto concern_copy = libmongoc::write_concern_copy.create_instance(); \
    concern_copy->interpose([](mongoc_write_concern_t const*) { return nullptr; }).forever();

#define MOCK_READ_PREFERENCE                                                                                       \
    auto read_prefs_get_hedge = libmongoc::read_prefs_get_hedge.create_instance();                                 \
    auto read_prefs_get_max_staleness_seconds = libmongoc::read_prefs_get_max_staleness_seconds.create_instance(); \
    auto read_prefs_get_mode = libmongoc::read_prefs_get_mode.create_instance();                                   \
    auto read_prefs_get_tags = libmongoc::read_prefs_get_tags.create_instance();                                   \
    auto read_prefs_set_hedge = libmongoc::read_prefs_set_hedge.create_instance();                                 \
    auto read_prefs_set_max_staleness_seconds = libmongoc::read_prefs_set_max_staleness_seconds.create_instance(); \
    auto read_prefs_set_mode = libmongoc::read_prefs_set_mode.create_instance();                                   \
    auto read_prefs_set_tags = libmongoc::read_prefs_set_tags.create_instance();                                   \
    ((void)0)
