#ifndef EASY_TEST_H_
#define EASY_TEST_H_

#include <easy_define.h>
#include <easy_list.h>
#include <easy_hash.h>
#include <easy_pool.h>
#include <easy_atomic.h>
#include <stdint.h>
#include <getopt.h>
#include <sys/time.h>
#include <easy_string.h>

EASY_CPP_START

typedef struct easy_test_func_t easy_test_func_t;
typedef struct easy_test_case_t easy_test_case_t;
typedef struct cmdline_param_t cmdline_param_t;
typedef void easy_test_func_pt();

struct easy_test_case_t {
    const char                *case_name;
    easy_test_func_pt         *fcsetup;
    easy_test_func_pt         *fcdown;
    easy_test_func_pt         *fsetup;
    easy_test_func_pt         *fdown;
    easy_list_t               listnode;
    easy_hash_list_t          hash_node;
    easy_list_t               list;
    int                       list_cnt;
};

// struct test
struct easy_test_func_t {
    easy_test_case_t          *tc;
    const char                *func_name;
    easy_test_func_pt         *func;
    easy_list_t               listnode;
    int                       ret;
};
// cmdline parameter
struct cmdline_param_t {
    const char                *filter_str;
    int                       filter_str_len;
    int                       filter_flags;
};

#define EASY_TEST_COLOR_RED   1
#define EASY_TEST_COLOR_GREEN 2

// extern
extern easy_pool_t      *easy_test_pool;
extern easy_hash_t      *easy_test_case_table;
extern easy_list_t      easy_test_case_list;
extern int              easy_test_retval;
extern easy_atomic_t    easy_test_alloc_byte;

// color printf
static inline void easy_test_color_printf(int color, const char *fmt, ...)
{
    va_list                 args;
    va_start(args, fmt);
    printf("\033[0;3%dm", color);
    vprintf(fmt, args);
    printf("\033[m");
    va_end(args);
}

static int64_t easy_test_now()
{
    struct timeval          tv;
    gettimeofday (&tv, 0);
    return 1000L * tv.tv_sec + tv.tv_usec / 1000;
}

static int easy_test_case_cmp(const void *a, const void *b)
{
    easy_test_case_t        *tc = (easy_test_case_t *) b;
    return strcmp(tc->case_name, (const char *)a);
}

static easy_test_case_t *easy_test_get_tc(const char *case_name)
{
    easy_test_case_t        *tc;
    uint64_t                key;

    if (easy_test_pool == NULL) {
        easy_test_pool = easy_pool_create(1024);
        easy_test_case_table = easy_hash_create(easy_test_pool, 128,
                                                offsetof(easy_test_case_t, hash_node));
    }

    key = easy_hash_code(case_name, strlen(case_name), 3);
    tc = (easy_test_case_t *)easy_hash_find_ex(easy_test_case_table, key, easy_test_case_cmp, case_name);

    if (tc == NULL) {
        tc = (easy_test_case_t *)easy_pool_calloc(easy_test_pool, sizeof(easy_test_case_t));
        tc->case_name = case_name;
        easy_list_init(&tc->list);
        easy_hash_add(easy_test_case_table, key, &tc->hash_node);
        easy_list_add_head(&tc->listnode, &easy_test_case_list);
    }

    return tc;
}

static inline void easy_test_reg_func(const char *case_name, const char *func_name,
                                      easy_test_func_pt *func, int before)
{
    easy_test_func_t        *t;
    easy_test_case_t        *tc;

    tc = easy_test_get_tc(case_name);
    t = (easy_test_func_t *)easy_pool_calloc(easy_test_pool, sizeof(easy_test_func_t));
    t->func_name = func_name;
    t->func = func;
    t->tc = tc;

    easy_list_add_head(&t->listnode, &tc->list);
    tc->list_cnt ++;
}

static inline void easy_test_print_usage(char *prog_name)
{
    fprintf(stderr, "%s [-f [-]filter_string]\n"
            "    -f, --filter            filter string\n"
            "    -l, --list              list tests\n"
            "    -h, --help              display this help and exit\n"
            "    -V, --version           version and build time\n\n", prog_name);
}

static inline void easy_test_print_list()
{
    easy_test_case_t        *tc;
    easy_test_func_t        *t;

    // foreach
    easy_list_for_each_entry(tc, &easy_test_case_list, listnode) {
        printf("%s.\n", tc->case_name);
        easy_list_for_each_entry(t, &tc->list, listnode) {
            printf("  %s.%s\n", tc->case_name, t->func_name);
        }
    }
}

static inline int easy_test_is_skip(const char *str, cmdline_param_t *cp)
{
    int                     ret;

    if (cp->filter_str_len > 0) {
        ret = strncmp(cp->filter_str, str, cp->filter_str_len);

        if ((ret != 0 && cp->filter_flags == 0) || (ret == 0 && cp->filter_flags != 0))
            return 1;
    } else if (cp->filter_str_len < 0) {
        ret = strcmp(cp->filter_str, str);

        if ((ret != 0 && cp->filter_flags == 0) || (ret == 0 && cp->filter_flags != 0))
            return 1;
    }

    return 0;
}

static inline int easy_test_parse_cmd_line(int argc, char *const argv[], cmdline_param_t *cp)
{
    int                     opt, len;
    const char              *opt_string = "hVf:l";
    struct option           long_opts[] = {
        {"filter", 1, NULL, 'f'},
        {"list", 0, NULL, 'l'},
        {"help", 0, NULL, 'h'},
        {"version", 0, NULL, 'V'},
        {0, 0, 0, 0}
    };

    opterr = 0;

    while ((opt = getopt_long(argc, argv, opt_string, long_opts, NULL)) != -1) {
        switch (opt) {
        case 'f':

            if (*optarg == '-') {
                cp->filter_flags = 1;
                cp->filter_str = optarg + 1;
            } else {
                cp->filter_str = optarg;
            }

            len = strlen(cp->filter_str);

            if (len > 0 && (cp->filter_str[len - 1] == '*' || cp->filter_str[len - 1] == '?'))
                cp->filter_str_len = len - 1;
            else
                cp->filter_str_len = -1;

            break;

        case 'l':
            easy_test_print_list();
            return EASY_ERROR;

        case 'V':
            fprintf(stderr, "BUILD_TIME: %s %s\n", __DATE__, __TIME__);
            return EASY_ERROR;

        case 'h':
        default:
            easy_test_print_usage(argv[0]);
            return EASY_ERROR;
        }
    }

    return EASY_OK;
}

static inline void *easy_test_realloc (void *ptr, size_t size)
{
    char                    *p1, *p = (char *)ptr;

    if (p) {
        p -= 8;
        easy_atomic_add(&easy_test_alloc_byte, -(*((int *)p)));
    }

    if (size) {
        easy_atomic_add(&easy_test_alloc_byte, size);
        p1 = (char *)easy_realloc(p, size + 8);

        if (p1) {
            *((int *)p1) = size;
            p1 += 8;
        }

        return p1;
    } else if (p) {
        //easy_free(p);
    }

    return NULL;
}

static int easy_test_exec_case(easy_test_case_t *tc)
{
    easy_test_func_t        *t;
    int64_t                 t1, t2;
    int                     failcnt = 0;

    easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[----------]");
    printf(" %d tests from %s\n", tc->list_cnt, tc->case_name);

    if (tc->fcsetup) (*tc->fcsetup)();

    easy_list_for_each_entry(t, &tc->list, listnode) {
        // start run
        easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[ RUN      ]");
        printf(" %s.%s\n", tc->case_name, t->func_name);

        easy_test_retval = 0;
        t1 = easy_test_now();

        if (tc->fsetup) (*tc->fsetup)();

        (t->func)();

        if (tc->fdown) (*tc->fdown)();

        t2 = easy_test_now();
        t->ret = easy_test_retval;

        // failure
        if (easy_test_retval) {
            easy_test_color_printf(EASY_TEST_COLOR_RED, "[  FAILED  ]");
            failcnt ++;
        } else {
            easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[       OK ]");
        }

        printf(" %s.%s (%d ms)\n", tc->case_name, t->func_name, (int)(t2 - t1));
    }

    if (tc->fcdown) (*tc->fcdown)();

    easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[----------]");
    printf(" %d tests from %s\n\n", tc->list_cnt, tc->case_name);
    return failcnt;
}

static inline int easy_test_main(int argc, char *argv[])
{
    easy_test_case_t        *tc, *tc1;
    easy_test_func_t        *t, *nt;
    int64_t                 t1, t2;
    int                     total_failcnt, total_func_cnt, total_case_cnt;
    char                    test_func_name[256];
    cmdline_param_t         cp;

    // parse cmd
    memset(&cp, 0, sizeof(cmdline_param_t));

    if (easy_test_parse_cmd_line(argc, argv, &cp) == EASY_ERROR) {
        return -1;
    }

    // init
    easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[==========]");

    total_func_cnt = 0;
    total_case_cnt = 0;
    total_failcnt = 0;
    easy_list_for_each_entry_safe(tc, tc1, &easy_test_case_list, listnode) {
        if (cp.filter_str_len) {
            easy_list_for_each_entry_safe(t, nt, &tc->list, listnode) {
                snprintf(test_func_name, 256, "%s.%s", tc->case_name, t->func_name);

                if (easy_test_is_skip(test_func_name, &cp)) {
                    easy_list_del(&t->listnode);
                    tc->list_cnt --;
                }
            }
        }

        if (tc->list_cnt == 0) {
            easy_list_del(&tc->listnode);
        } else {
            total_func_cnt += tc->list_cnt;
            total_case_cnt ++;
        }
    }

    printf(" Running %d tests from %d cases.\n", total_func_cnt, total_case_cnt);

    t1 = easy_test_now();
    easy_pool_set_allocator(realloc);
    easy_list_for_each_entry(tc, &easy_test_case_list, listnode) {
        total_failcnt += easy_test_exec_case(tc);
    }
    t2 = easy_test_now();

    easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[==========]");
    printf(" %d tests ran. (%d ms total)\n", total_func_cnt, (int)(t2 - t1));
    easy_test_color_printf(EASY_TEST_COLOR_GREEN, "[  PASSED  ]");
    printf(" %d tests.\n", total_func_cnt - total_failcnt);

    if (total_failcnt > 0) {
        easy_test_color_printf(EASY_TEST_COLOR_RED, "[  FAILED  ]");
        printf(" %d tests, listed below:\n", total_failcnt);
        easy_list_for_each_entry(tc, &easy_test_case_list, listnode) {
            easy_list_for_each_entry(t, &tc->list, listnode) {
                if (!t->ret) continue;

                easy_test_color_printf(EASY_TEST_COLOR_RED, "[  FAILED  ]");
                printf(" %s.%s\n", tc->case_name, t->func_name);
            }
        }

        printf(" %d FAILED TEST\n", total_failcnt);
    }

    if (easy_test_alloc_byte) {
        printf("no free memory: %ld\n", (long)easy_test_alloc_byte);
    }

    return (total_failcnt > 0 ? 1 : 0);
}

#define EASY_TEST_MAIN_DEFINE                                                           \
    int                     easy_test_retval = 0;                                                           \
    easy_atomic_t           easy_test_alloc_byte = 0;                                             \
    easy_pool_t             *easy_test_pool = NULL;                                                 \
    easy_hash_t             *easy_test_case_table = NULL;                                           \
    easy_list_t             easy_test_case_list = EASY_LIST_HEAD_INIT(easy_test_case_list);         \
    __attribute__((destructor)) void unregtest_main() {                                 \
        easy_pool_set_allocator(NULL);                                                  \
        if (easy_test_pool) easy_pool_destroy(easy_test_pool);                          \
    }

#define RUN_TEST_MAIN                                                                   \
    EASY_TEST_MAIN_DEFINE                                                               \
    int main(int argc, char *argv[]) {                                                  \
        return easy_test_main(argc, argv);                                              \
    }

#define TEST_NAME(case_name, func_name) easy_testf_##case_name##_##func_name
#define TEST_CASE(case_name, func_name) easy_testc_##case_name##_##func_name
// TEST
#define TEST(case_name, func_name)                                                      \
    void TEST_NAME(case_name, func_name)();                                             \
    __attribute__((constructor)) void easy_testg_##case_name##_##func_name() {          \
        easy_test_reg_func(#case_name, #func_name,                                      \
                           TEST_NAME(case_name, func_name), 1);                         \
    }                                                                                   \
    void TEST_NAME(case_name, func_name)()

#define TEST_SETUP_DOWN(case_name, func_name)                                           \
    void TEST_CASE(case_name, func_name)();                                             \
    __attribute__((constructor)) void easy_testd_##case_name##_##func_name() {          \
        easy_test_case_t        *tc = easy_test_get_tc(#case_name);                            \
        tc->f##func_name = TEST_CASE(case_name, func_name);                             \
    }                                                                                   \
    void TEST_CASE(case_name, func_name)()

#define TEST_CASE_SETUP(case_name) TEST_SETUP_DOWN(case_name, csetup)
#define TEST_CASE_DOWN(case_name) TEST_SETUP_DOWN(case_name, cdown)

#define TEST_SETUP(case_name) TEST_SETUP_DOWN(case_name, setup)
#define TEST_DOWN(case_name) TEST_SETUP_DOWN(case_name, down)

// TEST_FAIL
#define TEST_FAIL(fmt, args...)                                                         \
    printf("ERROR at %s:%d, TEST_FAIL: " fmt "\n",                                  \
           __FILE__, __LINE__, ## args); easy_test_retval = 1;

// EXPECT_TRUE
#define EXPECT_TRUE(c) if(!(c)) {                                                       \
        printf("ERROR at %s:%d, EXPECT_TRUE(" #c ")\n",                                 \
               __FILE__, __LINE__); easy_test_retval = 1;}

// EXPECT_FALSE
#define EXPECT_FALSE(c) if((c)) {                                                       \
        printf("ERROR at %s:%d, EXPECT_FALSE(" #c ")\n",                                \
               __FILE__, __LINE__); easy_test_retval = 1;}

EASY_CPP_END

#ifndef __cplusplus
// EXPECT_EQ
#define EXPECT_EQ(a, b) if(!((int64_t)(a)==(int64_t)(b))) {                                 \
        printf("ERROR at %s: %d, EXPECT_EQ(" #a ", " #b ") (a=%" PRId64 ",b=%" PRId64 ")\n",\
               __FILE__, __LINE__, (int64_t)(a), (int64_t)(b)); easy_test_retval = 1;}

// EXPECT_NE
#define EXPECT_NE(a, b) if(((int64_t)(a)==(int64_t)(b))) {                                  \
        printf("ERROR at %s: %d, EXPECT_NE(" #a ", " #b ") (a=%" PRId64 ",b=%" PRId64 ")\n",\
               __FILE__, __LINE__, (int64_t)(a), (int64_t)(b)); easy_test_retval = 1;}

#else

#include <string>
#define EASY_STRINGIFY(x) #x
#define EASY_TOSTRING(x) EASY_STRINGIFY(x)

// EXPECT_EQ
#define EXPECT_EQ(a, b)                                                                     \
    EXPECT_EASY_EQ(__FILE__ ":" EASY_TOSTRING(__LINE__) ", EXPECT_EQ(" #a ", " #b ")",  \
                   (a), (b), false)

// EXPECT_NE
#define EXPECT_NE(a, b)                                                                     \
    EXPECT_EASY_EQ(__FILE__ ":" EASY_TOSTRING(__LINE__) ", EXPECT_NE(" #a ", " #b ")",  \
                   (a), (b), true)

template <typename A, typename B>
static inline void EXPECT_EASY_EQ(const char *location, const A &a, const B &b, bool neq)
{
    if (!((int64_t)(a) == (int64_t)(b)) ^ neq) {
        printf("ERROR at %s (a=%" PRId64 ",b=%" PRId64 ")\n",
               location, (int64_t)(a), (int64_t)(b));
        easy_test_retval = 1;
    }
}

static inline void EXPECT_EASY_EQ(
    const char *location, const std::string &a, const std::string &b, bool neq)
{
    if (!(a == b) ^ neq) {
        printf("ERROR at %s\na=%s\nb=%s\n",
               location, a.c_str(), b.c_str());
        easy_test_retval = 1;
    }
}

static inline void EXPECT_EASY_EQ(
    const char *location, const std::string &a, const char *const bcs, bool neq)
{
    EXPECT_EASY_EQ(location, a, std::string(bcs), neq);
}

static inline void EXPECT_EASY_EQ(
    const char *location, const char *const acs, const std::string &b, bool neq)
{
    EXPECT_EASY_EQ(location, std::string(acs), b, neq);
}

static inline void EXPECT_EASY_EQ(
    const char *location, const char *const acs, const char *const bcs, bool neq)
{
    EXPECT_EASY_EQ(location, std::string(acs), std::string(bcs), neq);
}
#endif

#endif
