#include <gtest/gtest.h>
#include <unistd.h>
#include <stdio.h>
#include "zlib.h"
#include "md5/md5.h"
#include "http.h"
#include "http_decoder_private.h"
#include "brotli/decode.h"
#include "brotli/encode.h"
#include "event2/buffer.h"

#define ZIP_UNZIP_TEST_DATA_LEN (1024 * 1024)
void httpd_url_decode(const char *string, size_t length, char *ostring, size_t *olen);
TEST(http_url_decoder, none_encode)
{
    char decoded_url_buf[2048] = {};
    size_t decode_url_buf_len = sizeof(decoded_url_buf);
    const char *encode_url = "https://docs.geedge.net/#all-updates";
    size_t decoded_url_len = http_url_decode(encode_url, strlen(encode_url), decoded_url_buf, decode_url_buf_len);
    EXPECT_EQ(decoded_url_len, strlen("https://docs.geedge.net/#all-updates"));
    EXPECT_STREQ("https://docs.geedge.net/#all-updates", decoded_url_buf);
}

TEST(http_url_decoder, simple)
{
    char decoded_url_buf[2048] = {};
    size_t decoded_url_buf_len = sizeof(decoded_url_buf);
    const char *encode_url = "http://a.b.cn/%A1%B2%C3%D4";
    size_t decoded_url_len = http_url_decode(encode_url, strlen(encode_url), decoded_url_buf, decoded_url_buf_len);

    const unsigned char expect_result[] =
        {0x68, 0x74, 0x74, 0x70,
         0x3A, 0x2F, 0x2F, 0x61,
         0x2E, 0x62, 0x2E, 0x63,
         0x6E, 0x2F, 0xA1, 0xB2,
         0xC3, 0xD4, 0x00};
    EXPECT_EQ(decoded_url_len, sizeof(expect_result) - 1);
    EXPECT_EQ(0, memcmp(expect_result, decoded_url_buf, decoded_url_len));
}

TEST(http_url_decoder, chinese1)
{
    char decoded_url_buf[2048] = {};
    size_t decoded_url_buf_len = sizeof(decoded_url_buf);
    const char *encode_url = "http://www.baidu.com/%E6%B5%8B%E8%AF%95%E4%B8%AD%E6%96%87%E8%A7%A3%E7%A0%81";
    size_t decoded_url_len = http_url_decode(encode_url, strlen(encode_url), decoded_url_buf, decoded_url_buf_len);

    EXPECT_STREQ("http://www.baidu.com/\xE6\xB5\x8B\xE8\xAF\x95\xE4\xB8\xAD\xE6\x96\x87\xE8\xA7\xA3\xE7\xA0\x81", decoded_url_buf);
    EXPECT_EQ(decoded_url_len, strlen("http://www.baidu.com/\xE6\xB5\x8B\xE8\xAF\x95\xE4\xB8\xAD\xE6\x96\x87\xE8\xA7\xA3\xE7\xA0\x81"));
}

TEST(http_url_decoder, chinese2)
{
    char decoded_url_buf[2048];
    size_t decoded_url_buf_len = sizeof(decoded_url_buf);
    const char *encode_url = "http%3A%2F%2Fwww.baidu.com%2F%E7%BC%96%E8%A7%A3%E7%A0%81%E6%B5%8B%E8%AF%95%E5%93%88%E5%93%88";
    size_t decoded_url_len = http_url_decode(encode_url, strlen(encode_url), decoded_url_buf, decoded_url_buf_len);

    EXPECT_EQ(0, memcmp("http://www.baidu.com/\xE7\xBC\x96\xE8\xA7\xA3\xE7\xA0\x81\xE6\xB5\x8B\xE8\xAF\x95\xE5\x93\x88\xE5\x93\x88", decoded_url_buf, decoded_url_len));
    EXPECT_EQ(decoded_url_len, strlen("http://www.baidu.com/\xE7\xBC\x96\xE8\xA7\xA3\xE7\xA0\x81\xE6\xB5\x8B\xE8\xAF\x95\xE5\x93\x88\xE5\x93\x88"));
}

TEST(http, event_buffer)
{
    struct evbuffer *evbuf = evbuffer_new();

    evbuffer_add(evbuf, "hello", 5);

    size_t len = evbuffer_get_length(evbuf);
    EXPECT_EQ(len, 5);
    char outbuf[16];
    len = evbuffer_copyout(evbuf, outbuf, sizeof(outbuf));
    EXPECT_EQ(len, 5);
    EXPECT_EQ(0, memcmp(outbuf, "hello", 5));

    evbuffer_add(evbuf, ",", 1);
    evbuffer_add(evbuf, "world", 5);

    len = evbuffer_copyout(evbuf, outbuf, sizeof(outbuf));
    EXPECT_EQ(len, 11);
    EXPECT_EQ(0, memcmp(outbuf, "hello,world", 11));

    evbuffer_free(evbuf);
}

static int http_compress_use_deflate(unsigned char *indata, size_t indata_len, unsigned char *zip_data, size_t *zip_data_len)
{
#define ZIP_CHUNK 4096
#define UZIP_CHUNK 16384
    unsigned have;
    z_stream strm = {};

    /* allocate deflate state */
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;

    int ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 9, Z_DEFAULT_STRATEGY); /* no bad return value */
    if (ret != Z_OK)
    {
        return -1;
    }

    strm.next_in = indata;
    strm.avail_in = indata_len;

    size_t read_raw_data_len = 0;
    size_t ziped_len = 0;
    int flush_mode;
    do
    {
        /* run deflate() on input until output buffer not full, finish
           compression if all of source has been read in */
        strm.avail_in = MIN(indata_len - read_raw_data_len, ZIP_CHUNK);
        strm.next_in = indata + read_raw_data_len;
        flush_mode = (read_raw_data_len + ZIP_CHUNK < indata_len) ? Z_NO_FLUSH : Z_FINISH;
        do
        {
            unsigned char local_chunk_buf[UZIP_CHUNK];
            strm.avail_out = UZIP_CHUNK;
            strm.next_out = local_chunk_buf;
            ret = deflate(&strm, flush_mode);
            assert(ret != Z_STREAM_ERROR);
            have = UZIP_CHUNK - strm.avail_out;
            if (have > 0)
            {
                memcpy(zip_data + ziped_len, local_chunk_buf, have);
                ziped_len += have;
            }
        } while (strm.avail_out == 0);
        read_raw_data_len += ZIP_CHUNK;
    } while (read_raw_data_len < indata_len);

    *zip_data_len = ziped_len;
    assert(strm.avail_in == 0); /* all input will be used */
    /* done when last data in file processed */

    /* clean up and return */
    (void)deflateEnd(&strm);
    return Z_OK;
}

static int http_compress_use_brotli(unsigned char *input_data, size_t input_length, unsigned char *compressed_data, size_t *compressed_data_len)
{
    // Allocate memory for the compressed data
    size_t compressed_length = BrotliEncoderMaxCompressedSize(input_length);

    // Create a Brotli encoder state
    BrotliEncoderState *encoder = BrotliEncoderCreateInstance(NULL, NULL, NULL);
    if (!encoder)
    {
        fprintf(stderr, "Failed to create Brotli encoder.\n");
        return 1;
    }
    // Set up the Brotli encoder
    BrotliEncoderSetParameter(encoder, BROTLI_PARAM_QUALITY, 11);

    // Compress the data
    size_t available_in = input_length;
    const uint8_t *next_in = (const uint8_t *)input_data;
    size_t available_out = compressed_length;
    uint8_t *next_out = compressed_data;

    BrotliEncoderOperation op = BROTLI_OPERATION_FINISH;
    if (!BrotliEncoderCompressStream(encoder, op, &available_in, &next_in, &available_out, &next_out, NULL))
    {
        fprintf(stderr, "Brotli compression failed.\n");
        BrotliEncoderDestroyInstance(encoder);
        *compressed_data_len = 0;
        return -1;
    }
    // Calculate actual compressed size
    size_t actual_compressed_length = compressed_length - available_out;
    // printf("Original size: %zu\n", input_length);
    // printf("Compressed size: %zu\n", actual_compressed_length);

    // Clean up
    BrotliEncoderDestroyInstance(encoder);
    *compressed_data_len = actual_compressed_length;
    return 0;
}

/* High Compression Ratio */
static unsigned char *http_build_ascii_text(size_t data_len)
{
    unsigned char *raw_data = (unsigned char *)malloc(data_len);
    for (int i = 0; i < (int)data_len; i++)
    {
        raw_data[i] = 'A' + i % 26;
    }
    return raw_data;
}

/* Low Compression Ratio */
static unsigned char *http_build_random_content(size_t data_len)
{
    srand(12345678);
    unsigned char *raw_data = (unsigned char *)malloc(data_len);
    for (int i = 0; i < (int)data_len; i++)
    {
        raw_data[i] = (unsigned char)(rand() ^ i);
    }
    return raw_data;
}

static int http_zip_unzip_test(unsigned char *raw_data, size_t raw_data_len, enum http_content_encoding encoding, int input_zip_block_size)
{
    unsigned char raw_data_md5sum[16], zip_unzip_data_md5sum[16];
    MD5_CTX raw_data_md5_ctx = {}, zip_unzip_data_md5_ctx = {};

    MD5Init(&raw_data_md5_ctx);
    MD5Init(&zip_unzip_data_md5_ctx);

    MD5Update(&raw_data_md5_ctx, raw_data, raw_data_len);
    MD5Final(raw_data_md5sum, &raw_data_md5_ctx);

    unsigned char *zip_data = (unsigned char *)malloc(raw_data_len * 2);
    size_t zip_data_len = raw_data_len * 2;
    if (HTTP_CONTENT_ENCODING_DEFLATE == encoding)
    {
        http_compress_use_deflate(raw_data, raw_data_len, zip_data, &zip_data_len);
    }
    else
    {
        http_compress_use_brotli(raw_data, raw_data_len, zip_data, &zip_data_len);
    }

    struct http_content_decompress *decompress_ins = http_content_decompress_create(encoding);
    unsigned char *unzip_data;
    size_t unzip_data_len;

    size_t total_read_zip_size = 0;
    size_t total_unziped_data_len = 0;
    while (total_read_zip_size < zip_data_len)
    {
        unzip_data = NULL;
        unzip_data_len = 0;
        int decompress_input_len = MIN((size_t)input_zip_block_size, zip_data_len - total_read_zip_size);
        int ret = http_content_decompress_write(decompress_ins, (char *)zip_data + total_read_zip_size, decompress_input_len, (char **)&unzip_data, &unzip_data_len);
        if (ret < 0)
        {
            goto fail;
        }
        if (unzip_data && unzip_data_len)
        {
            MD5Update(&zip_unzip_data_md5_ctx, unzip_data, unzip_data_len);
            total_unziped_data_len += unzip_data_len;
        }
        total_read_zip_size += decompress_input_len;
    }

    free(zip_data);
    http_content_decompress_destroy(decompress_ins);
    MD5Final(zip_unzip_data_md5sum, &zip_unzip_data_md5_ctx);

    if (total_unziped_data_len != raw_data_len)
    {
        // printf("ERROR: zip-unzip data len:%zu, raw data len:%zu\n", total_unziped_data_len, raw_data_len);
        return -1;
    }
    return memcmp(raw_data_md5sum, zip_unzip_data_md5sum, 16);

fail:
    free(zip_data);
    http_content_decompress_destroy(decompress_ins);
    MD5Final(zip_unzip_data_md5sum, &zip_unzip_data_md5_ctx);
    return -1;
}

/* This test takes a long time! use gtest --gtest_also_run_disabled_tests to run this test if needs. */
TEST(http, DISABLED_deflate_ascii)
{
    int ret;
    unsigned char *raw_data = http_build_ascii_text(ZIP_UNZIP_TEST_DATA_LEN);
    for (int block = 1; block <= 1460; block++)
    {
        if (block % 10 == 0)
        {
            printf("raw content: ascii text, encoding: deflate, block range: %d - %d\n", block, block + 9);
        }
        ret = http_zip_unzip_test(raw_data, ZIP_UNZIP_TEST_DATA_LEN, HTTP_CONTENT_ENCODING_DEFLATE, block);
        if (ret != 0)
        {
            printf("raw content: ascii text, encoding: deflate, block size:%d, result = failed!\n", block);
        }
        EXPECT_EQ(0, ret);
    }
    free(raw_data);
}

/* This test takes a long time! use gtest --gtest_also_run_disabled_tests to run this test if needs. */
TEST(http, DISABLED_deflate_random)
{
    int ret;
    unsigned char *raw_data = http_build_random_content(ZIP_UNZIP_TEST_DATA_LEN);
    for (int block = 1; block <= 1460; block++)
    {
        if (block % 10 == 0)
        {
            printf("raw content: random binary, encoding: deflate, block range: %d - %d\n", block, block + 9);
        }
        ret = http_zip_unzip_test(raw_data, ZIP_UNZIP_TEST_DATA_LEN, HTTP_CONTENT_ENCODING_DEFLATE, block);
        if (ret != 0)
        {
            printf("raw content: random binary, encoding: deflate, block size:%d, result = failed!\n", block);
        }
        EXPECT_EQ(0, ret);
    }
    free(raw_data);
}

/* This test takes a long time! use gtest --gtest_also_run_disabled_tests to run this test if needs. */
TEST(http, DISABLED_brotli_random)
{
    int ret;
    unsigned char *raw_data = http_build_random_content(ZIP_UNZIP_TEST_DATA_LEN);
    for (int block = 33; block <= 1460; block++)
    {
        if (block % 10 == 0)
        {
            printf("raw content: random binary, encoding: brotli, block range: %d - %d\n", block, block + 9);
        }
        ret = http_zip_unzip_test(raw_data, ZIP_UNZIP_TEST_DATA_LEN, HTTP_CONTENT_ENCODING_BR, block);
        if (ret != 0)
        {
            printf("raw content: random binary, encoding: brotli, block size:%d, result = failed!\n", block);
        }
        EXPECT_EQ(0, ret);
    }
    free(raw_data);
}

/* This test takes a long time! use gtest --gtest_also_run_disabled_tests to run this test if needs. */
TEST(http, DISABLED_brotli_ascii)
{
    int ret;
    unsigned char *raw_data = http_build_ascii_text(ZIP_UNZIP_TEST_DATA_LEN);
    for (int block = 33; block <= 1460; block++)
    {
        if (block % 10 == 0)
        {
            printf("raw content: ascii text, encoding: brotli, block range: %d - %d\n", block, block + 9);
        }
        ret = http_zip_unzip_test(raw_data, ZIP_UNZIP_TEST_DATA_LEN, HTTP_CONTENT_ENCODING_BR, block);
        if (ret != 0)
        {
            printf("raw content: ascii text, encoding: brotli, block size:%d, result = failed!\n", block);
        }
        EXPECT_EQ(0, ret);
    }
    free(raw_data);
}

int main(int argc, char const *argv[])
{
    ::testing::InitGoogleTest(&argc, (char **)argv);
    return RUN_ALL_TESTS();
}
