/*
 * test_h2_parsing.c — Tests unitaires du fingerprinting HTTP/2 passif.
 *
 * Les fonctions testées (hpack_int_decode, h2_extract_pseudo_order,
 * h2_parse_preface_buf) sont réimplimentées localement pour éviter les
 * dépendances Apache/APR. La logique est identique à mod_reqin_log.c.
 */

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>

/* ====== Réimplémentation locale des fonctions H2 ====== */

static int hpack_int_decode(const unsigned char *buf, size_t len, int prefix,
                             size_t *pos, unsigned int *out)
{
    unsigned int mask = (1u << prefix) - 1u;
    unsigned int b, m;

    if (*pos >= len) return 0;
    *out = buf[(*pos)++] & mask;
    if (*out < mask) return 1;

    m = 0;
    while (*pos < len) {
        b = buf[(*pos)++];
        *out += (b & 0x7fu) << m;
        m += 7;
        if (!(b & 0x80u)) return 1;
        if (m > 28) return 0;
    }
    return 0;
}

static char h2_hpack_pseudo(unsigned int index)
{
    switch (index) {
        case 1:           return 'a';
        case 2: case 3:   return 'm';
        case 4: case 5:   return 'p';
        case 6: case 7:   return 's';
        default:          return 0;
    }
}

static void h2_extract_pseudo_order(const unsigned char *hpack, size_t len, char *out)
{
    size_t pos = 0;
    int out_pos = 0;
    int first = 1;

    while (pos < len && out_pos < 7) {
        unsigned char byte = hpack[pos];

        if (byte & 0x80u) {
            unsigned int idx = 0;
            if (!hpack_int_decode(hpack, len, 7, &pos, &idx)) break;
            if (idx == 0) break;

            char c = h2_hpack_pseudo(idx);
            if (!c) break;

            if (!first) out[out_pos++] = ',';
            out[out_pos++] = c;
            first = 0;

        } else if ((byte & 0xe0u) == 0x20u) {
            unsigned int sz = 0;
            if (!hpack_int_decode(hpack, len, 5, &pos, &sz)) break;
        } else {
            break;
        }
    }
    out[out_pos] = '\0';
}

/* Résultat de h2_parse_preface_buf — version allégée (pas d'APR) */
typedef struct {
    char settings[256];
    char wupdate[16];
    char pseudo[16];
    char fingerprint[512];
    int  has_priority;
    int  is_h2;
} h2_result_t;

static void h2_parse_preface_buf(const char *buf, size_t len, h2_result_t *res)
{
    static const char H2_MAGIC[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
    const size_t MAGIC_LEN = 24u;
    const size_t FRAME_HDR = 9u;

    memset(res, 0, sizeof(*res));
    strcpy(res->wupdate, "0");

    if (len < MAGIC_LEN || memcmp(buf, H2_MAGIC, MAGIC_LEN) != 0) return;

    int settings_out = 0;
    size_t pos = MAGIC_LEN;

    while (pos + FRAME_HDR <= len) {
        size_t frame_len = ((unsigned char)buf[pos]   << 16)
                         | ((unsigned char)buf[pos+1] <<  8)
                         |  (unsigned char)buf[pos+2];
        unsigned char type  = (unsigned char)buf[pos+3];
        unsigned char flags = (unsigned char)buf[pos+4];
        uint32_t stream_id  = (((unsigned char)buf[pos+5] & 0x7fu) << 24)
                             | ((unsigned char)buf[pos+6] << 16)
                             | ((unsigned char)buf[pos+7] <<  8)
                             |  (unsigned char)buf[pos+8];

        pos += FRAME_HDR;
        if (pos + frame_len > len) break;

        if (type == 0x04u && stream_id == 0 && !(flags & 0x01u)) {
            size_t sp = 0;
            while (sp + 6 <= frame_len &&
                   settings_out < (int)sizeof(res->settings) - 24) {
                uint16_t id  = ((unsigned char)buf[pos + sp]     << 8)
                             |  (unsigned char)buf[pos + sp + 1];
                uint32_t val = ((unsigned char)buf[pos + sp + 2] << 24)
                             | ((unsigned char)buf[pos + sp + 3] << 16)
                             | ((unsigned char)buf[pos + sp + 4] <<  8)
                             |  (unsigned char)buf[pos + sp + 5];
                sp += 6;
                if (settings_out > 0)
                    res->settings[settings_out++] = ',';
                settings_out += snprintf(res->settings + settings_out,
                                         (int)sizeof(res->settings) - settings_out,
                                         "%u:%u", id, val);
            }

        } else if (type == 0x08u && stream_id == 0) {
            if (frame_len >= 4) {
                uint32_t inc = (((unsigned char)buf[pos]   & 0x7fu) << 24)
                             | ((unsigned char)buf[pos+1]  << 16)
                             | ((unsigned char)buf[pos+2]  <<  8)
                             |  (unsigned char)buf[pos+3];
                snprintf(res->wupdate, sizeof(res->wupdate), "%u", inc);
            }

        } else if (type == 0x01u && stream_id > 0) {
            size_t hpack_start = 0;
            int parse_ok = 1;

            if ((flags & 0x08u) && parse_ok) {
                if (hpack_start >= frame_len) {
                    parse_ok = 0;
                } else {
                    unsigned char pad_len = (unsigned char)buf[pos + hpack_start++];
                    if (frame_len < hpack_start + (size_t)pad_len)
                        parse_ok = 0;
                    else
                        frame_len -= (size_t)pad_len;
                }
            }

            if ((flags & 0x20u) && parse_ok) {
                if (hpack_start + 5u > frame_len) {
                    parse_ok = 0;
                } else {
                    hpack_start += 5u;
                    res->has_priority = 1;
                }
            }

            if (parse_ok && hpack_start < frame_len) {
                h2_extract_pseudo_order(
                    (const unsigned char *)(buf + pos + hpack_start),
                    frame_len - hpack_start,
                    res->pseudo
                );
            }
            pos += frame_len;
            break;
        }
        pos += frame_len;
    }

    if (res->settings[0] != '\0') {
        res->is_h2 = 1;
        snprintf(res->fingerprint, sizeof(res->fingerprint), "%s|%s|%d|%s",
                 res->settings, res->wupdate, res->has_priority, res->pseudo);
    }
}

/* ====== Données de test : preface Chrome 120 ====== */

/*
 * Preface HTTP/2 Chrome 120 (capturée) :
 *   Magic (24 octets)
 *   SETTINGS frame : HEADER_TABLE_SIZE=65536, ENABLE_PUSH=0,
 *                    INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144
 *   WINDOW_UPDATE  : incrément 15663105
 *   HEADERS stream 1 : :method GET, :authority, :scheme https, :path /
 *                       → ordre HPACK indexé : 0x82(GET), 0x81(:auth), 0x87(https), 0x84(/)
 */
static const unsigned char CHROME_PREFACE[] = {
    /* Magic */
    'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n',
    '\r','\n','S','M','\r','\n','\r','\n',
    /* SETTINGS frame : length=24, type=0x04, flags=0x00, stream=0 */
    0x00, 0x00, 0x18,   /* length = 24 = 4×6 */
    0x04,               /* type SETTINGS */
    0x00,               /* flags = 0 */
    0x00, 0x00, 0x00, 0x00,  /* stream 0 */
    /* Entry 1: HEADER_TABLE_SIZE (1) = 65536 = 0x00010000 */
    0x00, 0x01,  0x00, 0x01, 0x00, 0x00,
    /* Entry 2: ENABLE_PUSH (2) = 0 */
    0x00, 0x02,  0x00, 0x00, 0x00, 0x00,
    /* Entry 3: INITIAL_WINDOW_SIZE (4) = 6291456 = 0x00600000 */
    0x00, 0x04,  0x00, 0x60, 0x00, 0x00,
    /* Entry 4: MAX_HEADER_LIST_SIZE (6) = 262144 = 0x00040000 */
    0x00, 0x06,  0x00, 0x04, 0x00, 0x00,
    /* WINDOW_UPDATE frame : length=4, type=0x08, flags=0, stream=0 */
    0x00, 0x00, 0x04,
    0x08,
    0x00,
    0x00, 0x00, 0x00, 0x00,
    /* increment = 15663105 = 0x00EF0001 */
    0x00, 0xEF, 0x00, 0x01,
    /* HEADERS frame : length=14, type=0x01, flags=0x05 (END_STREAM|END_HEADERS), stream=1 */
    0x00, 0x00, 0x0E,
    0x01,
    0x05,
    0x00, 0x00, 0x00, 0x01,
    /* HPACK : :method GET (0x82), :authority (0x81), :scheme https (0x87), :path / (0x84) */
    /* → ordre Chrome : m,a,s,p */
    0x82, 0x81, 0x87, 0x84,
    /* + quelques headers supplémentaires (indices statiques) */
    0x86,  /* :scheme http (index 6, régulier → stop après pseudo) */
    0x53,  /* accept (sans valeur — littéral, arrête le scan) */
    0x00, 0x05, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x68, 0x74, 0x6D, 0x6C
};

/* ====== Données de test : preface Firefox 120 ====== */

/*
 * Preface HTTP/2 Firefox 120 :
 *   SETTINGS: HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=131072, MAX_FRAME_SIZE=16384
 *   WINDOW_UPDATE: 12517377
 *   HEADERS: :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81)
 *            → ordre Firefox : m,p,s,a
 */
static const unsigned char FIREFOX_PREFACE[] = {
    /* Magic */
    'P','R','I',' ','*',' ','H','T','T','P','/','2','.','0','\r','\n',
    '\r','\n','S','M','\r','\n','\r','\n',
    /* SETTINGS frame : length=18, type=0x04, flags=0x00, stream=0 */
    0x00, 0x00, 0x12,
    0x04,
    0x00,
    0x00, 0x00, 0x00, 0x00,
    /* HEADER_TABLE_SIZE (1) = 65536 */
    0x00, 0x01,  0x00, 0x01, 0x00, 0x00,
    /* INITIAL_WINDOW_SIZE (4) = 131072 = 0x00020000 */
    0x00, 0x04,  0x00, 0x02, 0x00, 0x00,
    /* MAX_FRAME_SIZE (5) = 16384 = 0x00004000 */
    0x00, 0x05,  0x00, 0x00, 0x40, 0x00,
    /* WINDOW_UPDATE : increment = 12517377 = 0x00BF0001 */
    0x00, 0x00, 0x04,
    0x08,
    0x00,
    0x00, 0x00, 0x00, 0x00,
    0x00, 0xBF, 0x00, 0x01,
    /* HEADERS frame : length=4, type=0x01, flags=0x05, stream=1 */
    0x00, 0x00, 0x04,
    0x01,
    0x05,
    0x00, 0x00, 0x00, 0x01,
    /* HPACK : :method GET (0x82), :path / (0x84), :scheme https (0x87), :authority (0x81) */
    /* → ordre Firefox : m,p,s,a */
    0x82, 0x84, 0x87, 0x81
};

/* ====== Données de test : flux HTTP/1.1 (ne doit pas matcher) ====== */
static const char HTTP1_DATA[] =
    "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";

/* ====== Tests ====== */

static void test_chrome_settings_parsed(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);

    assert_int_equal(res.is_h2, 1);
    /* SETTINGS attendus : 1:65536,2:0,4:6291456,6:262144 */
    assert_string_equal(res.settings, "1:65536,2:0,4:6291456,6:262144");
}

static void test_chrome_window_update(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);

    assert_string_equal(res.wupdate, "15663105");
}

static void test_chrome_pseudo_order(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);

    /* Chrome : :method(m), :authority(a), :scheme(s), :path(p) */
    assert_string_equal(res.pseudo, "m,a,s,p");
}

static void test_chrome_fingerprint_akamai(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)CHROME_PREFACE, sizeof(CHROME_PREFACE), &res);

    assert_string_equal(res.fingerprint,
                        "1:65536,2:0,4:6291456,6:262144|15663105|0|m,a,s,p");
}

static void test_firefox_settings_parsed(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);

    assert_int_equal(res.is_h2, 1);
    assert_string_equal(res.settings, "1:65536,4:131072,5:16384");
}

static void test_firefox_pseudo_order(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);

    /* Firefox : :method(m), :path(p), :scheme(s), :authority(a) */
    assert_string_equal(res.pseudo, "m,p,s,a");
}

static void test_firefox_fingerprint_akamai(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf((const char *)FIREFOX_PREFACE, sizeof(FIREFOX_PREFACE), &res);

    assert_string_equal(res.fingerprint,
                        "1:65536,4:131072,5:16384|12517377|0|m,p,s,a");
}

static void test_http1_not_detected(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf(HTTP1_DATA, strlen(HTTP1_DATA), &res);

    assert_int_equal(res.is_h2, 0);
    assert_string_equal(res.settings, "");
    assert_string_equal(res.fingerprint, "");
}

static void test_empty_buffer_not_detected(void **state)
{
    (void)state;
    h2_result_t res;
    h2_parse_preface_buf("", 0, &res);

    assert_int_equal(res.is_h2, 0);
}

static void test_truncated_preface_no_crash(void **state)
{
    (void)state;
    h2_result_t res;
    /* Magic complet mais frame tronquée */
    h2_parse_preface_buf((const char *)CHROME_PREFACE, 30, &res);

    assert_int_equal(res.is_h2, 0);   /* SETTINGS incomplet → pas de fingerprint */
}

static void test_hpack_int_single_byte(void **state)
{
    (void)state;
    /* Entier 7-bit < 127 → encodé sur 1 octet */
    unsigned char buf[] = { 0x82 };   /* 0x80 | 2 → index=2 */
    size_t pos = 0;
    unsigned int out = 0;
    int ok = hpack_int_decode(buf, 1, 7, &pos, &out);

    assert_int_equal(ok, 1);
    assert_int_equal(out, 2);
    assert_int_equal(pos, 1);
}

static void test_hpack_pseudo_table(void **state)
{
    (void)state;
    assert_int_equal(h2_hpack_pseudo(1), 'a');
    assert_int_equal(h2_hpack_pseudo(2), 'm');
    assert_int_equal(h2_hpack_pseudo(3), 'm');
    assert_int_equal(h2_hpack_pseudo(4), 'p');
    assert_int_equal(h2_hpack_pseudo(5), 'p');
    assert_int_equal(h2_hpack_pseudo(6), 's');
    assert_int_equal(h2_hpack_pseudo(7), 's');
    assert_int_equal(h2_hpack_pseudo(8), 0);   /* header régulier */
    assert_int_equal(h2_hpack_pseudo(62), 0);
}

static void test_pseudo_order_extraction_direct(void **state)
{
    (void)state;
    /* HPACK block : :method(0x82), :path(0x84), :scheme(0x87), :authority(0x81) */
    unsigned char hpack[] = { 0x82, 0x84, 0x87, 0x81 };
    char out[16];
    h2_extract_pseudo_order(hpack, sizeof(hpack), out);

    assert_string_equal(out, "m,p,s,a");
}

static void test_pseudo_order_stops_at_regular_header(void **state)
{
    (void)state;
    /* :method(0x82), puis header régulier (0x88 = index 8) */
    unsigned char hpack[] = { 0x82, 0x88 };
    char out[16];
    h2_extract_pseudo_order(hpack, sizeof(hpack), out);

    assert_string_equal(out, "m");
}

/* ====== main ====== */

int main(void)
{
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(test_chrome_settings_parsed),
        cmocka_unit_test(test_chrome_window_update),
        cmocka_unit_test(test_chrome_pseudo_order),
        cmocka_unit_test(test_chrome_fingerprint_akamai),
        cmocka_unit_test(test_firefox_settings_parsed),
        cmocka_unit_test(test_firefox_pseudo_order),
        cmocka_unit_test(test_firefox_fingerprint_akamai),
        cmocka_unit_test(test_http1_not_detected),
        cmocka_unit_test(test_empty_buffer_not_detected),
        cmocka_unit_test(test_truncated_preface_no_crash),
        cmocka_unit_test(test_hpack_int_single_byte),
        cmocka_unit_test(test_hpack_pseudo_table),
        cmocka_unit_test(test_pseudo_order_extraction_direct),
        cmocka_unit_test(test_pseudo_order_stops_at_regular_header),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}
