/* ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include <apr_version.h>
#include <apr_strings.h>
#include <openssl/ssl.h>
#include <openssl/opensslv.h>

#if APR_VERSION_MAJOR < 2
#include <apu_version.h>
#endif


#include "serf.h"

/* These test cases have access to internal functions. */
#define SERF__TEST_INTERNAL     /* Disable SERF__CONV_assert() */
#include "serf_private.h"

#include "test_serf.h"

#define PER_CONN_UNKNOWN_KEY    SERF_CONFIG_PER_CONNECTION | 0xFF0001
/* #define PER_HOST_UNKNOWN_KEY    SERF_CONFIG_PER_HOST | 0xFF0002 */
#define PER_CONTEXT_UNKNOWN_KEY SERF_CONFIG_PER_CONTEXT | 0xFF0003
#define PER_CONN_TEST_KEY       SERF_CONFIG_PER_CONNECTION | 0xFFFFFF
#define PER_HOST_TEST_KEY       SERF_CONFIG_PER_HOST | 0xFFFFFF
#define PER_CONTEXT_TEST_KEY    SERF_CONFIG_PER_CONTEXT | 0xFFFFFF

static void test_config_store_per_context(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_config_t *cfg;

    serf_context_t *ctx = serf_context_create(tb->pool);

    /* The config store is empty initially, so we can play all we want */

    /* We don't have a serf connection yet, so only the per context config
       should be available to read and write */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_ctx_config(ctx, &cfg));
    CuAssertPtrEquals(tc, NULL, cfg->per_conn);
    CuAssertPtrEquals(tc, NULL, cfg->per_host);
    CuAssertPtrNotNull(tc, cfg->per_context);

    /* Get a non-existing key, value should be NULL */
    {
        const char *actual;

        CuAssertIntEquals(tc, APR_SUCCESS,
                          serf_config_get_string(cfg, PER_CONTEXT_UNKNOWN_KEY,
                                                 &actual));
        CuAssertPtrEquals(tc, NULL, actual);
    }

    /* Store and retrieve a string value for a per-context key */
    {
        const char *actual;

        CuAssertIntEquals(tc, APR_SUCCESS,
                          serf_config_set_string(cfg, PER_CONTEXT_TEST_KEY,
                                                 "test_value"));
        CuAssertIntEquals(tc, APR_SUCCESS,
                          serf_config_get_string(cfg, PER_CONTEXT_TEST_KEY,
                                                 &actual));
        CuAssertStrEquals(tc, "test_value", actual);
    }
}

/* Empty implementations, they won't be called. */
static void conn_closed(serf_connection_t *conn, void *closed_baton,
                        apr_status_t why, apr_pool_t *pool)
{
}

static apr_status_t conn_setup(apr_socket_t *skt,
                               serf_bucket_t **input_bkt,
                               serf_bucket_t **output_bkt,
                               void *setup_baton,
                               apr_pool_t *pool)
{
    return APR_SUCCESS;
}

static void test_config_store_per_connection_different_host(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_config_t *cfg1, *cfg2;
    apr_uri_t url;
    const char *actual;

    serf_context_t *ctx = serf_context_create(tb->pool);
    serf_connection_t *conn1, *conn2;

    /* Create two connections conn1 and conn2 to a different host */
    apr_uri_parse(tb->pool, "http://localhost:12345", &url);
    serf_connection_create2(&conn1, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);
    apr_uri_parse(tb->pool, "http://localhost:54321", &url);
    serf_connection_create2(&conn2, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);

    /* Test 1: This should return a config object with per_context, per_host and
       per_connection hash_table's initialized. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_conn_config(conn1, &cfg1));
    CuAssertPtrNotNull(tc, cfg1->per_context);
    CuAssertPtrNotNull(tc, cfg1->per_host);
    CuAssertPtrNotNull(tc, cfg1->per_conn);
    /* Get a config object for the other connection also. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_conn_config(conn2, &cfg2));

    /* Test 2: Get a non-existing per connection key, value should be NULL */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_CONN_UNKNOWN_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    /* Test 3: Store and retrieve a string value for a per-connection key */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg1, PER_CONN_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_CONN_TEST_KEY, &actual));
    CuAssertStrEquals(tc, "test_value", actual);

    /* Test that the key was set in the config for the first connection only. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg2, PER_CONN_TEST_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    /* Test 4: Store and retrieve a string value for a per-host key */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg1, PER_HOST_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_HOST_TEST_KEY, &actual));
    CuAssertStrEquals(tc, "test_value", actual);

    /* Test that the key was NOT set in the config for the second connection,
       since they are to a different host. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg2, PER_HOST_TEST_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);
}

static void test_config_store_per_connection_same_host(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_config_t *cfg1, *cfg2;
    apr_uri_t url;
    const char *actual;

    serf_context_t *ctx = serf_context_create(tb->pool);
    serf_connection_t *conn1, *conn2;

    /* Create two connections conn1 and conn2 to the same host */
    apr_uri_parse(tb->pool, "http://localhost:12345", &url);
    serf_connection_create2(&conn1, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);
    serf_connection_create2(&conn2, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);

    /* Test 1: This should return a config object with per_context, per_host and
     per_connection hash_table's initialized. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_conn_config(conn1, &cfg1));
    CuAssertPtrNotNull(tc, cfg1->per_context);
    CuAssertPtrNotNull(tc, cfg1->per_host);
    CuAssertPtrNotNull(tc, cfg1->per_conn);
    /* Get a config object for the other connection also. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_conn_config(conn2, &cfg2));

    /* Test 2: Get a non-existing per connection key, value should be NULL */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_CONN_UNKNOWN_KEY,
                                             &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    /* Test 3: Store and retrieve a string value for a per-connection key */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg1, PER_CONN_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_CONN_TEST_KEY, &actual));
    CuAssertStrEquals(tc, "test_value", actual);

    /* Test that the key was set in the config for the first connection only. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg2, PER_CONN_TEST_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    /* Test 4: Store and retrieve a string value for a per-host key */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg1, PER_HOST_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg1, PER_HOST_TEST_KEY, &actual));
    CuAssertStrEquals(tc, "test_value", actual);

    /* Test that the key was also set in the config for the second connection,
       since they are the same host. */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg2, PER_HOST_TEST_KEY, &actual));
    CuAssertStrEquals(tc, "test_value", actual);
}

static void test_config_store_error_handling(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_config_t *cfg;
    const char *actual;
    void *actual_obj;

    serf_context_t *ctx = serf_context_create(tb->pool);

    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_ctx_config(ctx, &cfg));

    /* Config only has per-context keys, check for no crashes when getting
       per-connection and per-host keys. */
    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_get_string(cfg, PER_HOST_TEST_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_get_string(cfg, PER_CONN_TEST_KEY, &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_set_string(cfg, PER_CONN_TEST_KEY,
                                             "test_value"));

    /* The same tests with objects instead of strings */
    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_get_object(cfg, PER_HOST_TEST_KEY, &actual_obj));
    CuAssertPtrEquals(tc, NULL, actual_obj);

    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_get_object(cfg, PER_CONN_TEST_KEY, &actual_obj));
    CuAssertPtrEquals(tc, NULL, actual_obj);

    CuAssertIntEquals(tc, APR_EINVAL,
                      serf_config_set_object(cfg, PER_CONN_TEST_KEY, stderr));
}

static void test_config_store_remove_objects(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_config_t *cfg;
    serf_connection_t *conn;
    apr_uri_t url;
    const char *actual;

    serf_context_t *ctx = serf_context_create(tb->pool);

    /* Create a connection conn */
    apr_uri_parse(tb->pool, "http://localhost:12345", &url);
    serf_connection_create2(&conn, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);

    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf__config_store_create_conn_config(conn, &cfg));

    /* Add and remove a key per-context */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg, PER_CONTEXT_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_remove_value(cfg, PER_CONTEXT_TEST_KEY));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg, PER_CONTEXT_TEST_KEY,
                                             &actual));
    CuAssertPtrEquals(tc, NULL, actual);

    /* Add and remove a key per-context */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg, PER_HOST_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_remove_value(cfg, PER_HOST_TEST_KEY));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg, PER_HOST_TEST_KEY,
                                             &actual));
    CuAssertPtrEquals(tc, NULL, actual);


    /* Add and remove a key per-context */
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_set_string(cfg, PER_CONN_TEST_KEY,
                                             "test_value"));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_remove_value(cfg, PER_CONN_TEST_KEY));
    CuAssertIntEquals(tc, APR_SUCCESS,
                      serf_config_get_string(cfg, PER_CONN_TEST_KEY,
                                             &actual));
    CuAssertPtrEquals(tc, NULL, actual);
}

/* Add and remove some headers from the headers bucket. */
/* Note: serf__bucket_headers_remove is an internal function */
static void test_header_buckets_remove(CuTest *tc)
{
    test_baton_t *tb = tc->testBaton;
    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
    const char *cur;

    serf_bucket_t *hdrs = serf_bucket_headers_create(alloc);
    CuAssertTrue(tc, hdrs != NULL);

    /* empty bucket, delete header */
    serf__bucket_headers_remove(hdrs, "Content-Length");

    /* bucket with one header, delete a non-existent header */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf__bucket_headers_remove(hdrs, "Content-Length");
    cur = "Content-Type: text/plain" CRLF CRLF;
    read_and_check_bucket(tc, hdrs, cur);

    serf_bucket_destroy(hdrs);
    hdrs = serf_bucket_headers_create(alloc);

    /* bucket with one header, delete it */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf__bucket_headers_remove(hdrs, "Content-Type");
    cur = CRLF;
    read_and_check_bucket(tc, hdrs, cur);

    serf_bucket_destroy(hdrs);
    hdrs = serf_bucket_headers_create(alloc);

    /* bucket with two headers, delete the first */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf_bucket_headers_set(hdrs, "Content-Length", "100");
    serf__bucket_headers_remove(hdrs, "Content-Type");
    cur = "Content-Length: 100" CRLF CRLF;
    read_and_check_bucket(tc, hdrs, cur);

    serf_bucket_destroy(hdrs);
    hdrs = serf_bucket_headers_create(alloc);

    /* bucket with two headers, delete the second */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf_bucket_headers_set(hdrs, "Content-Length", "100");
    serf__bucket_headers_remove(hdrs, "Content-Length");
    cur = "Content-Type: text/plain" CRLF CRLF;
    read_and_check_bucket(tc, hdrs, cur);

    serf_bucket_destroy(hdrs);
    hdrs = serf_bucket_headers_create(alloc);

    /* bucket with more headers, delete one in the middle */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf_bucket_headers_set(hdrs, "Content-Location", "/");
    serf_bucket_headers_set(hdrs, "Content-Length", "100");
    serf__bucket_headers_remove(hdrs, "Content-Location");
    cur = "Content-Type: text/plain" CRLF
    "Content-Length: 100" CRLF CRLF;
    read_and_check_bucket(tc, hdrs, cur);

    serf_bucket_destroy(hdrs);
    hdrs = serf_bucket_headers_create(alloc);

    /* bucket with more headers, delete more in the middle */
    serf_bucket_headers_set(hdrs, "Content-Type", "text/plain");
    serf_bucket_headers_set(hdrs, "Content-Location", "/");
    serf_bucket_headers_set(hdrs, "Content-Location", "/bla");
    serf_bucket_headers_set(hdrs, "Content-Length", "100");
    serf_bucket_headers_set(hdrs, "Content-Location", "/blub");
    serf__bucket_headers_remove(hdrs, "Content-Location");
    cur = "Content-Type: text/plain" CRLF
    "Content-Length: 100" CRLF CRLF;
    read_and_check_bucket(tc, hdrs, cur);
    serf_bucket_destroy(hdrs);
}

static void test_runtime_versions(CuTest *tc)
{
  apr_version_t version_of_apr;
#if APR_MAJOR_VERSION < 2
  apr_version_t version_of_aprutil;
#endif

  apr_version(&version_of_apr);

  CuAssertIntEquals(tc, APR_MAJOR_VERSION, version_of_apr.major);
  CuAssertTrue(tc, version_of_apr.minor >= APR_MINOR_VERSION);

  if (version_of_apr.minor == APR_MINOR_VERSION)
    CuAssertTrue(tc, version_of_apr.patch >= APR_PATCH_VERSION);

#if APR_MAJOR_VERSION < 2
  apu_version(&version_of_aprutil);

  CuAssertIntEquals(tc, APU_MAJOR_VERSION, version_of_aprutil.major);
  CuAssertTrue(tc, version_of_aprutil.minor >= APU_MINOR_VERSION);

  if (version_of_aprutil.minor == APU_MINOR_VERSION)
    CuAssertTrue(tc, version_of_aprutil.patch >= APU_PATCH_VERSION);

  CuAssertIntEquals(tc, APR_MAJOR_VERSION, APU_MAJOR_VERSION);
#endif
}

static void test_narrowing_conversions(CuTest *tc)
{
    int val;

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, 0);
    CuAssertIntEquals(tc, 0, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, 0);
    CuAssertIntEquals(tc, 0, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, 1);
    CuAssertIntEquals(tc, 1, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, 1);
    CuAssertIntEquals(tc, 1, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, -1);
    CuAssertIntEquals(tc, INT_MAX, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, -1);
    CuAssertIntEquals(tc, -1, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, INT_MAX);
    CuAssertIntEquals(tc, INT_MAX, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, INT_MAX);
    CuAssertIntEquals(tc, INT_MAX, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, INT_MIN);
    CuAssertIntEquals(tc, 0, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, INT_MIN);
    CuAssertIntEquals(tc, INT_MIN, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, APR_INT64_MAX);
    CuAssertIntEquals(tc, INT_MAX, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, APR_INT64_MAX);
    CuAssertIntEquals(tc, -1, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, APR_INT64_MIN);
    CuAssertIntEquals(tc, 0, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, APR_INT64_MIN);
    CuAssertIntEquals(tc, 0, val);

    SERF__POSITIVE_TO_INT(val, apr_uint64_t, APR_UINT64_MAX);
    CuAssertIntEquals(tc, INT_MAX, val);
    SERF__SIGNED_TO_INT(val, apr_int64_t, APR_UINT64_MAX);
    CuAssertIntEquals(tc, -1, val);
}


struct expected_attrs
{
    const char *token;
    const char *value;
};

static void parse_parameters(CuTest *tc, const char *attrs,
                             const struct expected_attrs expected[])
{
    test_baton_t *const tb = tc->testBaton;
    apr_hash_t *const dict = serf__parse_authn_parameters(attrs, tb->pool);
    const short attr_count = apr_hash_count(dict);
    int i;

    for (i = 0; expected[i].token; ++i)
    {
        const char *token = expected[i].token;
        const char *value = apr_hash_get(dict, token, APR_HASH_KEY_STRING);
        CuAssertStrEquals(tc, expected[i].value, value);
    }
    CuAssertIntEquals(tc, i, attr_count);
}

static void test_parse_parameters(CuTest *tc)
{
    static const struct expected_attrs expected[] = {
        { "realm", "Wonderland" },
        { "scope", "Alice" },
        { "!#$%&'*+-.^_`|~", "(\"\\)"},
        { "empty", "" },
        { NULL, NULL }
    };

    parse_parameters(tc,
                     "Realm=\"Wonderland\","
                     "ScOpE=Alice , "
                     "!#$%&'*+-.^_`|~=\"(\\\"\\\\)\","
                     "empty=\"\"",
                     expected);
}

static void test_parse_bad_parameters(CuTest *tc)
{
    static const struct expected_attrs unexpected[] = {
        { "first", "value" },
        { NULL, NULL }
    };
    static const struct expected_attrs *expected = &unexpected[1];

    parse_parameters(tc, "", expected);
    parse_parameters(tc, "\t", expected);
    parse_parameters(tc, "(comm", expected);
    parse_parameters(tc, "first=value, key=", unexpected);
    parse_parameters(tc, "key=\"value", expected);
    parse_parameters(tc, "key = value", expected);
    parse_parameters(tc, "key=\"value1\"key=value2", expected);
    parse_parameters(tc, "key=value1 key=value2", expected);
}

static void test_parse_repeated_parameters(CuTest *tc)
{
    static const struct expected_attrs expected[] = {
        { "key", "value2" },
        { NULL, NULL }
    };

    parse_parameters(tc, "key=value1, key=value2", expected);
}

static void test_parse_single_token_parameters(CuTest *tc)
{
    static const struct expected_attrs expected[] = {
        { "", "Alice/In+Wonderland.==" },
        { NULL, NULL }
    };

    parse_parameters(tc, "\tAlice/In+Wonderland.== ", expected);
    parse_parameters(tc, "Alice=In+Wonderland.=", &expected[1]);
}

static void test_parameter_case_folding(CuTest *tc)
{
    static const struct expected_attrs expected[] = {
        { "01234abcdefghijklmnopqrstuvwxyz56789", "Val" },
        { NULL, NULL }
    };

    parse_parameters(tc, "01234abcdefghijklmnopqrstuvwxyz56789=Val", expected);
    parse_parameters(tc, "01234ABCDEFGHIJKLMNOPQRSTUVWXYZ56789=Val", expected);
}

static void test_find_token(CuTest *tc)
{
    CuAssertPtrNotNull(tc, serf__find_token("foo", 0, "foo"));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, "foo"));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, " foo"));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, "foo "));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, "bar\tfoo"));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, "foo\tbar"));
    CuAssertPtrNotNull(tc, serf__find_token("foo", 3, "bar\t #$@*&^! foo qux"));
    CuAssertPtrEquals(tc, NULL, serf__find_token("foo", 2, "foo"));
    CuAssertPtrEquals(tc, NULL, serf__find_token("foo", 3, "\vfoo"));
    CuAssertPtrEquals(tc, NULL, serf__find_token("foo", 3, "foobar"));
    CuAssertPtrEquals(tc, NULL, serf__find_token("foo", 3, " qux foobar baz"));
    CuAssertPtrEquals(tc, NULL, serf__find_token("foo", 3, " qux bar"));
}

/*
 * Error callbacks
 */

struct error_callback_baton
{
    apr_pool_t *pool;
    apr_array_header_t *list;
};

static apr_status_t
error_cb_request_setup(serf_request_t *request,
                       void *setup_baton,
                       serf_bucket_t **req_bkt,
                       serf_response_acceptor_t *acceptor,
                       void **acceptor_baton,
                       serf_response_handler_t *handler,
                       void **handler_baton,
                       apr_pool_t *pool)
{
    return APR_SUCCESS;
}

static apr_status_t
error_cb_error_callback(void *baton,
                        unsigned source,
                        apr_status_t status,
                        const char *message)
{
    const struct error_callback_baton *const ecb = baton;
    const char *const msg = apr_psprintf(
        ecb->pool, "<%d> %c%c%c%c%c %s", status,
        ((source & SERF_ERROR_CB_SSL_CONTEXT) ? '*' : '-'),
        ((source & SERF_ERROR_CB_GLOBAL) ? 'g' : '-'),
        ((source & SERF_ERROR_CB_CONTEXT) ? 'c' : '-'),
        ((source & SERF_ERROR_CB_OUTGOING) ? 'o'
         : ((source & SERF_ERROR_CB_INCOMING) ? 'i' : '-')),
        ((source & SERF_ERROR_CB_REQUEST) ? 'q'
         : ((source & SERF_ERROR_CB_RESPONSE) ? 'p' : '-')),
        message);
    APR_ARRAY_PUSH(ecb->list, const char*) = msg;
    return APR_SUCCESS;
}

static void test_global_error_callback(CuTest *tc)
{
    test_baton_t *const tb = tc->testBaton;
    struct error_callback_baton ecb;
    serf_context_t *ctx;
    serf_connection_t *conn;
    serf_request_t *req;
    apr_uri_t url;

    ecb.pool = tb->pool;
    ecb.list = apr_array_make(ecb.pool, 3, sizeof(const char*));
    serf_global_error_callback_set(error_cb_error_callback, &ecb);

    /* Create a context and connection. */
    ctx = serf_context_create(ecb.pool);
    apr_uri_parse(tb->pool, "http://localhost:12345", &url);
    serf_connection_create2(&conn, ctx, url, conn_setup, NULL,
                            conn_closed, NULL, tb->pool);
    req = serf_connection_request_create(conn, error_cb_request_setup, NULL);

    serf__context_error(ctx, APR_SUCCESS, "context error");
    serf__connection_error(conn, SERF_ERROR_CLOSING, "connection error");
    serf__request_error(req, SERF_ERROR_REQUEST_LOST, "request error");

    /* Reset the error callback before checking the test results. */
    serf_global_error_callback_set(serf__global_error_callback, NULL);

    CuAssertPtrEquals(tc, ctx, conn->ctx);
    CuAssertIntEquals(tc, 3, ecb.list->nelts);
    CuAssertStrEquals(tc, "<0> --c-- context error",
                      APR_ARRAY_IDX(ecb.list, 0, const char*));
    CuAssertStrEquals(tc, "<120101> ---o- connection error",
                      APR_ARRAY_IDX(ecb.list, 1, const char*));
    CuAssertStrEquals(tc, "<120102> ---oq request error",
                      APR_ARRAY_IDX(ecb.list, 2, const char*));
}


CuSuite *test_internal(void)
{
    CuSuite *suite = CuSuiteNew();

    CuSuiteSetSetupTeardownCallbacks(suite, test_setup, test_teardown);

    SUITE_ADD_TEST(suite, test_config_store_per_context);
    SUITE_ADD_TEST(suite, test_config_store_per_connection_different_host);
    SUITE_ADD_TEST(suite, test_config_store_per_connection_same_host);
    SUITE_ADD_TEST(suite, test_config_store_error_handling);
    SUITE_ADD_TEST(suite, test_config_store_remove_objects);
    SUITE_ADD_TEST(suite, test_header_buckets_remove);
    SUITE_ADD_TEST(suite, test_runtime_versions);
    SUITE_ADD_TEST(suite, test_narrowing_conversions);
    SUITE_ADD_TEST(suite, test_parse_parameters);
    SUITE_ADD_TEST(suite, test_parse_bad_parameters);
    SUITE_ADD_TEST(suite, test_parse_repeated_parameters);
    SUITE_ADD_TEST(suite, test_parse_single_token_parameters);
    SUITE_ADD_TEST(suite, test_parameter_case_folding);
    SUITE_ADD_TEST(suite, test_find_token);
    SUITE_ADD_TEST(suite, test_global_error_callback);

    return suite;
}
