#include "config.h"
#include "cunit/unit.h"
#include "xmalloc.h"
#include "util.h"
#include "hash.h"
#include "imap/jmap_util.h"

static void test_patchobject(void)
{
#define TESTCASE(_from, _dest, _want, _flags) \
    { \
        unsigned myflags = (_flags); \
        json_t *jfrom = json_loads((_from), JSON_DECODE_ANY, NULL); \
        json_t *jdest = json_loads((_dest), JSON_DECODE_ANY, NULL); \
        json_t *jwant = json_loads((_want), JSON_DECODE_ANY, NULL); \
        json_t *jdiff = jmap_patchobject_create(jfrom, jdest, myflags); \
        CU_ASSERT_PTR_NOT_NULL(jdiff); \
        char *swant = json_dumps(jwant, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        char *sdiff = json_dumps(jdiff, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        CU_ASSERT_STRING_EQUAL(swant, sdiff); \
        json_t *jback = jmap_patchobject_apply(jfrom, jdiff, NULL, myflags); \
        char *sback = json_dumps(jback, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        char *sdest = json_dumps(jdest, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        CU_ASSERT_STRING_EQUAL(sdest, sback); \
        free(sdest); \
        free(sback); \
        json_decref(jback); \
        free(sdiff); \
        free(swant); \
        json_decref(jdiff); \
        json_decref(jwant); \
        json_decref(jdest); \
        json_decref(jfrom); \
    }

    const char *from, *dest, *want;

    /* Remove one property at top-level */
    from = "{"
    "  \"a\": 1,"
    "  \"b\": 1"
    "}";
    dest = "{"
    "  \"a\": 1"
    "}";
    want = "{"
    "  \"b\": null"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Add one property at top-level */
    from = "{"
    "  \"a\": 1"
    "}";
    dest = "{"
    "  \"a\": 1,"
    "  \"b\": 1"
    "}";
    want = "{"
    "  \"b\": 1"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Replace one scalar property at top-level with another */
    from = "{"
    "  \"a\": 1"
    "}";
    dest = "{"
    "  \"a\": 2"
    "}";
    want = "{"
    "  \"a\": 2"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Replace one object property at top-level with a scalar */
    from = "{"
    "  \"a\": {"
    "    \"b\": 1"
    "  }"
    "}";
    dest = "{"
    "  \"a\": 2"
    "}";
    want = "{"
    "  \"a\": 2"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Replace one scalar property at top-level with an object */
    from = "{"
    "  \"a\": {"
    "    \"b\": 1"
    "  }"
    "}";
    dest = "{"
    "  \"a\": 2"
    "}";
    want = "{"
    "  \"a\": 2"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Add a nested property */
    from = "{"
    "  \"a\": {"
    "    \"b\": 1"
    "  }"
    "}";
    dest = "{"
    "  \"a\": {"
    "    \"b\": 1,"
    "    \"c\": 2"
    "  }"
    "}";
    want = "{"
    "  \"a/c\": 2"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Remove a nested property */
    from = "{"
    "  \"a\": {"
    "    \"b\": 1,"
    "    \"c\": 2"
    "  }"
    "}";
    dest = "{"
    "  \"a\": {"
    "    \"b\": 1"
    "  }"
    "}";
    want = "{"
    "  \"a/c\": null"
    "}";
    TESTCASE(from, dest, want, 0);

    /* Changing array member replaces array */
    from = "{"
    "  \"a\": [{"
    "    \"val\": \"foo\""
    "  }, {"
    "    \"val\": \"bar\""
    "  }]"
    "}";
    dest = "{"
    "  \"a\": [{"
    "    \"val\": \"foo\""
    "  }, {"
    "    \"val\": \"baz\""
    "  }]"
    "}";
    want = "{"
    "  \"a\": [{"
    "    \"val\": \"foo\""
    "  }, {"
    "    \"val\": \"baz\""
    "  }]"
    "}";
    TESTCASE(from, dest, want, 0);

    /* PATCH_ALLOW_ARRAY: replaces array member */
    from = "{"
    "  \"a\": [{"
    "    \"val\": \"foo\""
    "  }, {"
    "    \"val\": \"bar\""
    "  }]"
    "}";
    dest = "{"
    "  \"a\": [{"
    "    \"val\": \"foo\""
    "  }, {"
    "    \"val\": \"baz\""
    "  }]"
    "}";
    want = "{"
    "  \"a/1/val\": \"baz\""
    "}";
    TESTCASE(from, dest, want, PATCH_ALLOW_ARRAY);

#undef TESTCASE
}

static void test_patchobject_invalid(void)
{
#define TESTCASE(_from, _patch, _want_invalid, _flags) \
    { \
        unsigned myflags = (_flags); \
        json_t *jfrom = json_loads((_from), JSON_DECODE_ANY, NULL); \
        json_t *jpatch = json_loads((_patch), JSON_DECODE_ANY, NULL); \
        json_t *jwant_invalid = json_loads((_want_invalid), JSON_DECODE_ANY, NULL); \
        json_t *jhave_invalid = json_array(); \
        json_t *jhave = jmap_patchobject_apply(jfrom, jpatch, jhave_invalid, myflags); \
        CU_ASSERT_PTR_NULL(jhave); \
        char *want_invalid = json_dumps(jwant_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        char *have_invalid = json_dumps(jhave_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
        CU_ASSERT_STRING_EQUAL(want_invalid, have_invalid); \
        free(have_invalid); \
        free(want_invalid); \
        json_decref(jhave_invalid); \
        json_decref(jwant_invalid); \
        json_decref(jpatch); \
        json_decref(jfrom); \
    }

    const char *from, *patch, *want_invalid;

    /* Set non-existent member */
    from = "{"
    "  \"a\": \"foo\""
    "}";
    patch = "{"
    "  \"x/y\": \"bar\""
    "}";
    want_invalid = "["
    "\"x/y\""
    "]";
    TESTCASE(from, patch, want_invalid, 0);

    /* Remove non-existent member */
    from = "{"
    "  \"a\": \"foo\""
    "}";
    patch = "{"
    "  \"x/y\": null"
    "}";
    want_invalid = "["
    "\"x/y\""
    "]";
    TESTCASE(from, patch, want_invalid, 0);

    /* Patch inside array - but no PATCH_ALLOW_ARRAY flag */
    from = "{"
    "  \"a\": ["
    "    \"foo\","
    "    \"bar\""
    "  ]"
    "}";
    patch = "{"
    "  \"a/1\": \"bam\""
    "}";
    want_invalid = "["
    "  \"a/1\" "
    "]";
    TESTCASE(from, patch, want_invalid, 0);

    /* Delete from array */
    from = "{"
    "  \"a\": ["
    "    \"foo\","
    "    \"bar\""
    "  ]"
    "}";
    patch = "{"
    "  \"a/1\": null"
    "}";
    want_invalid = "["
    "  \"a/1\" "
    "]";
    TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

    /* Patch non-existent array entry */
    from = "{"
    "  \"a\": ["
    "    \"foo\","
    "    \"bar\""
    "  ]"
    "}";
    patch = "{"
    "  \"a/2\": \"bam\""
    "}";
    want_invalid = "["
    "  \"a/2\" "
    "]";
    TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

    /* Patch with special JSON pointer '-' */
    from = "{"
    "  \"a\": ["
    "    \"foo\","
    "    \"bar\""
    "  ]"
    "}";
    patch = "{"
    "  \"a/-\": \"bam\""
    "}";
    want_invalid = "["
    "  \"a/-\" "
    "]";
    TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

#undef TESTCASE
}

static void test_decode_to_utf8(void)
{
    struct testcase {
        const char *data;
        const char *charset;
        int encoding;
        float confidence;
        const char *want_val;
        int want_is_encoding_problem;
    };

    // this is all about "Adélaïde"

    struct testcase tcs[] = {{
        // ISO-8859-1 encoded data claims to be UTF-8, confidence 0.51
#ifdef HAVE_LIBCHARDET
        "Ad""\xe9""la""\xef""de",
        "utf-8",
        ENCODING_NONE,
        0.51,
        "Ad""\xc3\xa9""la""\xc3\xaf""de",
        1
#else
        "Ad""\xe9""la""\xef""de",
        "utf-8",
        ENCODING_NONE,
        0.51,
        "Ad""\xef\xbf\xbd""la""\xef\xbf\xbd""de",
        1
#endif
    }, {
        // ISO-8859-1 encoded data claims to be UTF-8, confidence 1.0
        "Ad""\xe9""la""\xef""de",
        "utf-8",
        ENCODING_NONE,
        1.0,
        "Ad""\xef\xbf\xbd""la""\xef\xbf\xbd""de",
        1
    }, {
        // Fast-path valid UTF-8
        "Ad""\xc3\xa9""la""\xc3\xaf""de",
        "utf-8",
        ENCODING_NONE,
        0.51,
        "Ad""\xc3\xa9""la""\xc3\xaf""de",
        0
    }, {
        // Fast-path valid UTF-8 with replacement chars
        "Ad""\xef\xbf\xbd""la""\xef\xbf\xbd""de",
        "utf-8",
        ENCODING_NONE,
        0.51,
        "Ad""\xef\xbf\xbd""la""\xef\xbf\xbd""de",
        0
    }, {
        // Multi-byte UTF-8 with invalid byte sequence:
        // "Hello,😛🪐world🍎🏓!💾," "\xc3\x28"
        "SGVsbG8s8J+Ym/CfqpB3b3JsZPCfjY7wn4+TIfCfkr4swyg=",
        "utf-8",
        ENCODING_BASE64,
        0.0,
        "\x48\x65\x6c\x6c\x6f\x2c\xf0\x9f\x98\x9b\xf0\x9f"
        "\xaa\x90\x77\x6f\x72\x6c\x64\xf0\x9f\x8d\x8e\xf0"
        "\x9f\x8f\x93\x21\xf0\x9f\x92\xbe"
        "," "\xef\xbf\xbd" "(",
        1
    }, {
        NULL, NULL, ENCODING_UNKNOWN, 0.0, NULL, 0
    }};

    struct buf buf = BUF_INITIALIZER;
    struct testcase *tc;
    for (tc = tcs; tc->data; tc++) {
        int is_problem = 0;
        buf_reset(&buf);

        jmap_decode_to_utf8(tc->charset, tc->encoding,
            tc->data, strlen(tc->data), tc->confidence, &buf, &is_problem);
        if (tc->want_val)
            CU_ASSERT_STRING_EQUAL(tc->want_val, buf_cstring(&buf));
        else
            CU_ASSERT_EQUAL(0, buf_len(&buf));
        CU_ASSERT_EQUAL(tc->want_is_encoding_problem, is_problem);
    }
    buf_free(&buf);
}

static void test_caleventid(void)
{

#define TESTCASE(s, want_uid, want_recurid, want_eid) \
    { \
        struct jmap_caleventid *eid = jmap_caleventid_decode(s); \
        CU_ASSERT_PTR_NOT_NULL_FATAL(eid); \
        if (want_uid) \
            CU_ASSERT_STRING_EQUAL(want_uid, eid->ical_uid); \
        else \
            CU_ASSERT_PTR_NULL(eid->ical_uid); \
        CU_ASSERT_PTR_EQUAL(eid->ical_uid, eid->_alloced[0]); \
        if (want_recurid) \
            CU_ASSERT_STRING_EQUAL(want_recurid, eid->ical_recurid); \
        else \
            CU_ASSERT_PTR_NULL(eid->ical_recurid); \
        CU_ASSERT_PTR_EQUAL(eid->ical_recurid, eid->_alloced[1]); \
        struct buf buf = BUF_INITIALIZER; \
        CU_ASSERT_PTR_NOT_NULL(jmap_caleventid_encode(eid, &buf)); \
        if (want_eid) \
            CU_ASSERT_STRING_EQUAL(want_eid, buf_cstring(&buf)); \
        else \
            CU_ASSERT_STRING_EQUAL(s, buf_cstring(&buf)); \
        buf_free(&buf); \
        jmap_caleventid_free(&eid); \
        CU_ASSERT_PTR_NULL(eid); \
    }

    TESTCASE("61928725-A80F-4B2B-9704-ECC71E58F9E1",
        "61928725-A80F-4B2B-9704-ECC71E58F9E1", NULL,
        "E-61928725-A80F-4B2B-9704-ECC71E58F9E1");

    TESTCASE("E-61928725-A80F-4B2B-9704-ECC71E58F9E1",
        "61928725-A80F-4B2B-9704-ECC71E58F9E1", NULL, NULL);

    TESTCASE("ER-20211207-61928725-A80F-4B2B-9704-ECC71E58F9E1",
        "61928725-A80F-4B2B-9704-ECC71E58F9E1", "20211207", NULL);

    TESTCASE("EB-NjE5Mjg3MjVAZXhhbXBsZS5jb20",
        "61928725@example.com", NULL, NULL);

    TESTCASE("ERB-20211207T233000-NjE5Mjg3MjVAZXhhbXBsZS5jb20",
        "61928725@example.com", "20211207T233000", NULL);

}

/* vim: set ft=c: */
