/*
 * Copyright (c) 2018, Intel Corporation
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of Intel Corporation nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "gtest/gtest.h"
#include "chimera/ch.h"

static char garbage[] = "TEST(HybridArgChecks, DatabaseSizeNoDatabase) {" \
                        "    size_t sz = ch_database_size(0);" \
                        "    ASSERT_EQ(0, sz);";

namespace /* anonymous */ {

// Dummy callback: does nothing, returns 0 (keep matching)
ch_callback_t dummyHandler(unsigned, unsigned long long,
                           unsigned long long, unsigned, unsigned,
                           const ch_capture_t *, void *) {
    // empty
    return CH_CALLBACK_CONTINUE;
}

// Helper: correctly construct a simple database.
static
void makeDatabase(ch_database_t **hydb) {
    static const char *expr[] = { "foo.*bar" };
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    err = ch_compile_multi(expr, nullptr, nullptr, 1, 0, nullptr, &db,
                           &compile_err);

    ASSERT_EQ(CH_SUCCESS, err);
    ASSERT_TRUE(db != nullptr);

    *hydb = db;
}

// Helper: given a database, build me some scratch.
static
void makeScratch(const ch_database_t *db,
                 ch_scratch_t **scratch) {
    ch_error_t err = ch_alloc_scratch(db, scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    ASSERT_TRUE(*scratch != nullptr);
}

// Break the magic number of the given database.
void breakDatabaseMagic(ch_database *db) {
    // database magic should be 0xdbdb at the start
    ASSERT_TRUE(memcmp("\xde\xde", db, 2) == 0);
    *(char *)db = 0xdc;
}

// Break the version number of the given database.
void breakDatabaseVersion(ch_database *db) {
    // database version is the second u32
    *((char *)db + 4) += 1;
}

// Check that CH_version gives us a reasonable string back
TEST(HybridArgChecks, Version) {
    const char *version = ch_version();
    ASSERT_TRUE(version != nullptr);
    ASSERT_TRUE(version[0] >= '0' && version[0] <= '9')
        << "First byte should be a digit.";
    ASSERT_EQ('.', version[1]) << "Second byte should be a dot.";
}

// ch_compile: Hand the compiler a bogus flag.
TEST(HybridArgChecks, SingleBogusFlags) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badflags[] = {
        0xffffffff,
        16,
        128,
        256,
        512,
    };

    for (size_t i = 0; i < sizeof(badflags)/sizeof(badflags[0]); i++) {
        const char expr[]  = "foobar";
        err = ch_compile(expr, badflags[i], 0, nullptr, &db, &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Unrecognized flag used.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile: Hand the compiler a bogus mode.
TEST(HybridArgChecks, SingleBogusMode) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badModes[] = {
        0xffffffff,
        1,
        2,
        CH_MODE_GROUPS << 1, // this was our largest mode flag
    };

    for (size_t i = 0; i < sizeof(badModes)/sizeof(badModes[0]); i++) {
        const char expr[]  = "foobar";
        err = ch_compile(expr, 0,  badModes[i], nullptr, &db, &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Invalid mode flag supplied.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile: Compile a nullptr pattern set)
TEST(HybridArgChecks, SingleCompileBlockNoPattern) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;
    err = ch_compile(nullptr, 0, 0, nullptr, &db, &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(db == nullptr);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile: Compile a pattern to a nullptr database ptr
TEST(HybridArgChecks, SingleCompileBlockNoDatabase) {
    ch_compile_error_t *compile_err = nullptr;
    const char expr[] = "foobar";
    ch_error_t err;
    err = ch_compile(expr, 0, 0, nullptr, nullptr, &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_multi: Hand the compiler a bogus flag.
TEST(HybridArgChecks, MultiBogusFlags) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badflags[] = {
        0xffffffff,
        16, // HS_FLAG_ERROREOD
        128,
        256,
        512,
    };

    for (size_t i = 0; i < sizeof(badflags)/sizeof(badflags[0]); i++) {
        const char *expr[]  = { "foobar" };
        err = ch_compile_multi(expr, &badflags[i], nullptr, 1, 0, nullptr, &db,
                               &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Unrecognized flag used.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile_multi: Hand the ch_compile_multi a bogus mode.
TEST(HybridArgChecks, MultiBogusMode) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badModes[] = {
        0xffffffff,
        1,
        2,
        CH_MODE_GROUPS << 1, // this was our largest mode flag
    };

    for (size_t i = 0; i < sizeof(badModes)/sizeof(badModes[0]); i++) {
        const char *expr[]  = { "foobar" };
        err = ch_compile_multi(expr, nullptr, nullptr, 1, badModes[i], nullptr,
                               &db, &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Invalid mode flag supplied.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile_multi: Compile a nullptr pattern set (block mode)
TEST(HybridArgChecks, MultiCompileBlockNoPattern) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;
    err = ch_compile_multi(nullptr, nullptr, nullptr, 1, 0, nullptr, &db,
                           &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(db == nullptr);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_multi: Compile a set of zero patterns
TEST(HybridArgChecks, MultiCompileZeroPatterns) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    const char *expr[] = {"foobar"};
    ch_error_t err;
    err = ch_compile_multi(expr, nullptr, nullptr, 0, 0, nullptr, &db,
                           &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(db == nullptr);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_multi: Compile a pattern to a nullptr database ptr
TEST(HybridArgChecks, MultiCompileBlockNoDatabase) {
    ch_compile_error_t *compile_err = nullptr;
    const char *expr[] = {"foobar"};
    ch_error_t err;
    err = ch_compile_multi(expr, nullptr, nullptr, 1, 0, nullptr, nullptr,
                           &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_ext_multi: Hand the compiler a bogus flag.
TEST(HybridArgChecks, ExtMultiBogusFlags) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badflags[] = {
        0xffffffff,
        16, // HS_FLAG_ERROREOD
        128,
        256,
        512,
    };

    for (size_t i = 0; i < sizeof(badflags)/sizeof(badflags[0]); i++) {
        const char *expr[]  = { "foobar" };
        err = ch_compile_ext_multi(expr, &badflags[i], nullptr, 1, 0,
                                   10000000, 8000, nullptr, &db, &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Unrecognized flag used.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile_ext_multi: Hand the ch_compile_multi a bogus mode.
TEST(HybridArgChecks, ExtMultiBogusMode) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;

    static const unsigned int badModes[] = {
        0xffffffff,
        1,
        2,
        CH_MODE_GROUPS << 1, // this was our largest mode flag
    };

    for (size_t i = 0; i < sizeof(badModes)/sizeof(badModes[0]); i++) {
        const char *expr[]  = { "foobar" };
        err = ch_compile_ext_multi(expr, nullptr, nullptr, 1, badModes[i],
                                   10000000, 8000, nullptr, &db, &compile_err);
        EXPECT_EQ(CH_COMPILER_ERROR, err);
        EXPECT_TRUE(db == nullptr);
        EXPECT_TRUE(compile_err != nullptr);
        EXPECT_STREQ("Invalid mode flag supplied.", compile_err->message);
        ch_free_compile_error(compile_err);
    }
}

// ch_compile_ext_multi: Compile a nullptr pattern set (block mode)
TEST(HybridArgChecks, ExtMultiCompileBlockNoPattern) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    ch_error_t err;
    err = ch_compile_ext_multi(nullptr, nullptr, nullptr, 1, 0, 10000000,
                               8000, nullptr, &db, &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(db == nullptr);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_ext_multi: Compile a set of zero patterns
TEST(HybridArgChecks, ExtMultiCompileZeroPatterns) {
    ch_database_t *db = nullptr;
    ch_compile_error_t *compile_err = nullptr;
    const char *expr[] = {"foobar"};
    ch_error_t err;
    err = ch_compile_ext_multi(expr, nullptr, nullptr, 0, 0, 10000000,
                               8000, nullptr, &db, &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(db == nullptr);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_compile_ext_multi: Compile a pattern to a nullptr database ptr
TEST(HybridArgChecks, ExtMultiCompileBlockNoDatabase) {
    ch_compile_error_t *compile_err = nullptr;
    const char *expr[] = {"foobar"};
    ch_error_t err;
    err = ch_compile_ext_multi(expr, nullptr, nullptr, 1, 0, 10000000,
                           8000, nullptr, nullptr, &compile_err);
    EXPECT_EQ(CH_COMPILER_ERROR, err);
    EXPECT_TRUE(compile_err != nullptr);
    ch_free_compile_error(compile_err);
}

// ch_scan: Call with no database
TEST(HybridArgChecks, ScanBlockNoDatabase) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);
    ch_scratch_t *scratch = nullptr;
    makeScratch(db, &scratch);

    ch_error_t err = ch_scan(nullptr, "data", 4, 0, scratch,
                             dummyHandler, nullptr, nullptr);
    ASSERT_NE(CH_SUCCESS, err);
    EXPECT_NE(CH_SCAN_TERMINATED, err);

    // teardown
    err = ch_free_scratch(scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    ch_free_database(db);
}

// ch_scan: Call with a database with broken magic
TEST(HybridArgChecks, ScanBlockBrokenDatabaseMagic) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);
    ch_scratch_t *scratch = nullptr;
    makeScratch(db, &scratch);

    // break the database here, after scratch alloc
    breakDatabaseMagic(db);

    ch_error_t err = ch_scan(db, "data", 4, 0, scratch,
                             dummyHandler, nullptr, nullptr);
    ASSERT_EQ(CH_INVALID, err);

    // teardown
    err = ch_free_scratch(scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    free(db);
}

// ch_scan: Call with a database with broken version
TEST(HybridArgChecks, ScanBlockBrokenDatabaseVersion) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);
    ch_scratch_t *scratch = nullptr;
    makeScratch(db, &scratch);

    // break the database here, after scratch alloc
    breakDatabaseVersion(db);

    ch_error_t err = ch_scan(db, "data", 4, 0, scratch,
                             dummyHandler, nullptr, nullptr);
    ASSERT_EQ(CH_DB_VERSION_ERROR, err);

    // teardown
    err = ch_free_scratch(scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    ch_free_database(db);
}

// ch_scan: Call with no data
TEST(HybridArgChecks, ScanBlockNoData) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);
    ch_scratch_t *scratch = nullptr;
    makeScratch(db, &scratch);

    ch_error_t err = ch_scan(db, nullptr, 4, 0, scratch, dummyHandler,
                             nullptr, nullptr);
    ASSERT_NE(CH_SUCCESS, err);
    EXPECT_NE(CH_SCAN_TERMINATED, err);

    // teardown
    err = ch_free_scratch(scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    ch_free_database(db);
}

// ch_scan: Call with no scratch
TEST(HybridArgChecks, ScanBlockNoScratch) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    ch_error_t err = ch_scan(db, "data", 4, 0, nullptr, dummyHandler,
                             nullptr, nullptr);
    ASSERT_NE(CH_SUCCESS, err);
    EXPECT_NE(CH_SCAN_TERMINATED, err);

    // teardown
    ch_free_database(db);
}

// ch_scan: Call with no event handler
TEST(HybridArgChecks, ScanBlockNoHandler) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);
    ch_scratch_t *scratch = nullptr;
    makeScratch(db, &scratch);

    ch_error_t err = ch_scan(db, "data", 4, 0, scratch, nullptr, nullptr,
                             nullptr);
    ASSERT_EQ(CH_SUCCESS, err);
    EXPECT_NE(CH_SCAN_TERMINATED, err);

    // teardown
    err = ch_free_scratch(scratch);
    ASSERT_EQ(CH_SUCCESS, err);
    ch_free_database(db);
}

// ch_alloc_scratch: Call with no database
TEST(HybridArgChecks, AllocScratchNoDatabase) {
    ch_scratch_t *scratch = nullptr;
    ch_error_t err = ch_alloc_scratch(nullptr, &scratch);
    EXPECT_NE(CH_SUCCESS, err);
    EXPECT_TRUE(scratch == nullptr);
}

// ch_alloc_scratch: Call with nullptr ptr-to-scratch
TEST(HybridArgChecks, AllocScratchNullScratchPtr) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    ch_error_t err = ch_alloc_scratch(db, nullptr);
    ASSERT_EQ(CH_INVALID, err);

    // teardown
    ch_free_database(db);
}

// ch_alloc_scratch: Call with bogus scratch
TEST(HybridArgChecks, AllocScratchBogusScratch) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    ch_scratch_t *blah = (ch_scratch_t *)malloc(100);
    ASSERT_TRUE(blah != nullptr);
    memset(blah, 0xf0, 100);
    ch_error_t err = ch_alloc_scratch(db, &blah);
    ASSERT_EQ(CH_INVALID, err);

    // teardown
    free(blah);
    ch_free_database(db);
}

// ch_alloc_scratch: Call with broken database magic
TEST(HybridArgChecks, AllocScratchBadDatabaseMagic) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    breakDatabaseMagic(db);

    ch_scratch_t *scratch = nullptr;
    ch_error_t err = ch_alloc_scratch(db, &scratch);
    ASSERT_EQ(CH_INVALID, err);

    // teardown
    free(db);
}

// ch_alloc_scratch: Call with broken database version
TEST(HybridArgChecks, AllocScratchBadDatabaseVersion) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    breakDatabaseVersion(db);

    ch_scratch_t *scratch = nullptr;
    ch_error_t err = ch_alloc_scratch(db, &scratch);
    ASSERT_EQ(CH_DB_VERSION_ERROR, err);

    // teardown
    ch_free_database(db);
}

// ch_clone_scratch: Call with no source scratch
TEST(HybridArgChecks, CloneScratchNoSource) {
    ch_scratch_t *scratch = nullptr, *scratch2 = nullptr;
    ch_error_t err = ch_clone_scratch(scratch, &scratch2);
    EXPECT_NE(CH_SUCCESS, err);
    EXPECT_TRUE(scratch2 == nullptr);
}

// ch_database_size: Call with no database
TEST(HybridArgChecks, DatabaseSizeNoDatabase) {
    size_t sz = 0;
    ch_error_t err = ch_database_size(0, &sz);
    ASSERT_EQ(CH_INVALID, err);
    ASSERT_EQ(0U, sz);
}

// ch_clone_scratch: bad scratch arg
TEST(HybridArgChecks, CloneBadScratch) {
    // Try cloning the scratch
    void *local_garbage = malloc(sizeof(garbage));
    ASSERT_TRUE(local_garbage != nullptr);
    memcpy(local_garbage, garbage, sizeof(garbage));
    ch_scratch_t *cloned = nullptr;
    ch_scratch_t *scratch = (ch_scratch_t *)local_garbage;
    ch_error_t err = ch_clone_scratch(scratch, &cloned);
    free(local_garbage);
    ASSERT_EQ(CH_INVALID, err);
}

// ch_scan: bad scratch arg
TEST(HybridArgChecks, ScanBadScratch) {
    ch_database_t *db = nullptr;
    makeDatabase(&db);

    void *local_garbage = malloc(sizeof(garbage));
    ASSERT_TRUE(local_garbage != nullptr);
    memcpy(local_garbage, garbage, sizeof(garbage));

    ch_scratch_t *scratch = (ch_scratch_t *)local_garbage;
    ch_error_t err = ch_scan(db, "data", 4, 0, scratch,
                             dummyHandler, nullptr, nullptr);
    free(local_garbage);
    ASSERT_EQ(CH_INVALID, err);

    // teardown
    ch_free_database(db);
}

TEST(HybridArgChecks, ch_free_database_null) {
    ch_error_t err = ch_free_database(nullptr);
    ASSERT_EQ(CH_SUCCESS, err);
}

TEST(HybridArgChecks, ch_free_database_garbage) {
    ch_error_t err = ch_free_database((ch_database_t *)garbage);
    ASSERT_EQ(CH_INVALID, err);
}

TEST(HybridArgChecks, ch_free_scratch_null) {
    ch_error_t err = ch_free_scratch(nullptr);
    ASSERT_EQ(CH_SUCCESS, err);
}

TEST(HybridArgChecks, ch_free_scratch_garbage) {
    ch_error_t err = ch_free_scratch((ch_scratch_t *)garbage);
    ASSERT_EQ(CH_INVALID, err);
}

TEST(HybridArgChecks, ch_free_compile_error_null) {
    ch_error_t err = ch_free_compile_error(nullptr);
    ASSERT_EQ(CH_SUCCESS, err);
}

} // namespace

