/* ====================================================================
 *    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_pools.h>

#include "serf.h"
#include "serf_bucket_util.h"
#include "test_serf.h"

/* This bucket uses a list of count - data/len - status actions (provided by the
   test case), to control the read / read_iovec operations. */
typedef struct mockbkt_context_t {
    mockbkt_action *actions;
    int len;
    const char *current_data;
    int remaining_data;
    int current_action;
    int remaining_times;
} mockbkt_context_t;

serf_bucket_t *serf_bucket_mock_create(mockbkt_action *actions,
                                       int len,
                                       serf_bucket_alloc_t *allocator)
{
    mockbkt_context_t *ctx;

    ctx = serf_bucket_mem_alloc(allocator, sizeof(*ctx));
    ctx->actions = actions;
    ctx->len = len;
    ctx->current_data = 0l;
    ctx->remaining_data = -1;
    ctx->current_action = 0;
    ctx->remaining_times = -1;

    return serf_bucket_create(&serf_bucket_type_mock, allocator, ctx);
}

static apr_status_t next_action(mockbkt_context_t *ctx)
{
    mockbkt_action *action;

    while (1)
    {
        if (ctx->current_action >= ctx->len)
            return APR_EOF;

        action = &ctx->actions[ctx->current_action];

        if (ctx->remaining_times == 0) {
            ctx->current_action++;
            ctx->remaining_times = -1;
            ctx->remaining_data = -1;
            continue;
        }

        if (ctx->remaining_data <= 0) {
            ctx->current_data = action->data;
            ctx->remaining_times = action->times;
            ctx->remaining_data = strlen(action->data);
        }

        return APR_SUCCESS;
    }
}

static apr_status_t serf_mock_readline(serf_bucket_t *bucket,
                                       int acceptable, int *found,
                                       const char **data, apr_size_t *len)
{
    mockbkt_context_t *ctx = bucket->data;
    mockbkt_action *action;
    apr_status_t status;
    const char *start_line;

    status = next_action(ctx);
    if (status) {
        *len = 0;
        return status;
    }

    action = &ctx->actions[ctx->current_action];
    start_line = *data = ctx->current_data;
    *len = ctx->remaining_data;

    serf_util_readline(&start_line, len, acceptable, found);

    /* See how much ctx->current moved forward. */
    *len = start_line - ctx->current_data;
    ctx->remaining_data -= *len;
    ctx->current_data += *len;
    if (ctx->remaining_data == 0)
        ctx->remaining_times--;

    return ctx->remaining_data ? APR_SUCCESS : action->status;
}

static apr_status_t serf_mock_read(serf_bucket_t *bucket,
                                   apr_size_t requested,
                                   const char **data, apr_size_t *len)
{
    mockbkt_context_t *ctx = bucket->data;
    mockbkt_action *action;
    apr_status_t status;

    status = next_action(ctx);
    if (status) {
        *len = 0;
        return status;
    }

    action = &ctx->actions[ctx->current_action];
    *len = requested < ctx->remaining_data ? requested : ctx->remaining_data;
    *data = ctx->current_data;

    ctx->remaining_data -= *len;
    ctx->current_data += *len;

    if (ctx->remaining_data == 0)
        ctx->remaining_times--;

    return ctx->remaining_data ? APR_SUCCESS : action->status;
}

static apr_status_t serf_mock_peek(serf_bucket_t *bucket,
                                   const char **data,
                                   apr_size_t *len)
{
    mockbkt_context_t *ctx = bucket->data;
    mockbkt_action *action;
    apr_status_t status;

    status = next_action(ctx);
    if (status)
        return status;

    action = &ctx->actions[ctx->current_action];
    *len = ctx->remaining_data;
    *data = ctx->current_data;

    /* peek only returns an error, APR_EOF or APR_SUCCESS.
       APR_EAGAIN is returned as APR_SUCCESS. */
    if (SERF_BUCKET_READ_ERROR(action->status))
        return status;

    return action->status == APR_EOF ? APR_EOF : APR_SUCCESS;
}

/* An action { "", 0, APR_EAGAIN } means that serf should exit serf_context_run
   and pass the buck back to the application. As long as no new data arrives,
   this action remains active.

   This function allows the 'application' to trigger the arrival of more data.
   If the current action is { "", 0, APR_EAGAIN }, reduce the number of times
   the action should run by one, and proceed with the next action if needed.
 */
apr_status_t serf_bucket_mock_more_data_arrived(serf_bucket_t *bucket)
{
    mockbkt_context_t *ctx = bucket->data;
    mockbkt_action *action;
    apr_status_t status;

    status = next_action(ctx);
    if (status)
        return status;

    action = &ctx->actions[ctx->current_action];
    if (ctx->remaining_data == 0 && APR_STATUS_IS_EAGAIN(action->status)) {
        ctx->remaining_times--;
        action->times--;
    }

    return APR_SUCCESS;
}

static void serf_mock_destroy(serf_bucket_t *bucket)
{
#ifndef SERF_DEBUG_BUCKET_USE
  serf_default_destroy_and_data(bucket);
#else
  /* This bucket is impossible to drain. Avoid abort() */
  serf_bucket_mem_free(bucket->allocator, bucket->data);
  serf_bucket_mem_free(bucket->allocator, bucket);
#endif
}

const serf_bucket_type_t serf_bucket_type_mock = {
    "MOCK",
    serf_mock_read,
    serf_mock_readline,
    serf_default_read_iovec,
    serf_default_read_for_sendfile,
    serf_default_read_bucket,
    serf_mock_peek,
    serf_mock_destroy
};


/* internal test for the mock buckets */
static void test_basic_mock_bucket(CuTest *tc)
{
    serf_bucket_t *mock_bkt;
    test_baton_t *tb = tc->testBaton;
    serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool);
    /* read one line */
    {
        mockbkt_action actions[]= {
            { 1, "HTTP/1.1 200 OK" CRLF, APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions, 1, alloc);
        read_and_check_bucket(tc, mock_bkt,
                              "HTTP/1.1 200 OK" CRLF);
        serf_bucket_destroy(mock_bkt);

        mock_bkt = serf_bucket_mock_create(actions, 1, alloc);
        readlines_and_check_bucket(tc, mock_bkt, SERF_NEWLINE_CRLF,
                                   "HTTP/1.1 200 OK" CRLF, 1);
        serf_bucket_destroy(mock_bkt);
    }
    /* read one line, character per character */
    {
        apr_status_t status;
        const char *expected = "HTTP/1.1 200 OK" CRLF;
        mockbkt_action actions[]= {
            { 1, "HTTP/1.1 200 OK" CRLF, APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions, 1, alloc);
        do
        {
            const char *data;
            apr_size_t len;

            status = serf_bucket_read(mock_bkt, 1, &data, &len);
            CuAssert(tc, "Got error during bucket reading.",
                     !SERF_BUCKET_READ_ERROR(status));
            CuAssert(tc, "Read more data than expected.",
                     strlen(expected) >= len);
            CuAssert(tc, "Read data is not equal to expected.",
                     strncmp(expected, data, len) == 0);
            CuAssert(tc, "Read more data than requested.",
                     len <= 1);

            expected += len;
        } while(!APR_STATUS_IS_EOF(status));

        CuAssert(tc, "Read less data than expected.", strlen(expected) == 0);
        serf_bucket_destroy(mock_bkt);
    }
    /* read multiple lines */
    {
        mockbkt_action actions[]= {
            { 1, "HTTP/1.1 200 OK" CRLF, APR_SUCCESS },
            { 1, "Content-Type: text/plain" CRLF, APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions, 2, alloc);
        readlines_and_check_bucket(tc, mock_bkt, SERF_NEWLINE_CRLF,
                                   "HTTP/1.1 200 OK" CRLF
                                   "Content-Type: text/plain" CRLF, 2);
        serf_bucket_destroy(mock_bkt);
    }
    /* read empty line */
    {
        mockbkt_action actions[]= {
            { 1, "HTTP/1.1 200 OK" CRLF, APR_SUCCESS },
            { 1, "", APR_EAGAIN },
            { 1, "Content-Type: text/plain" CRLF, APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions, 3, alloc);
        read_and_check_bucket(tc, mock_bkt,
                              "HTTP/1.1 200 OK" CRLF
                              "Content-Type: text/plain" CRLF);
        serf_bucket_destroy(mock_bkt);
        mock_bkt = serf_bucket_mock_create(actions, 3, alloc);
        readlines_and_check_bucket(tc, mock_bkt, SERF_NEWLINE_CRLF,
                                   "HTTP/1.1 200 OK" CRLF
                                   "Content-Type: text/plain" CRLF, 2);
        serf_bucket_destroy(mock_bkt);
    }
    /* read empty line */
    {
        mockbkt_action actions[]= {
            { 1, "HTTP/1.1 200 OK" CR, APR_SUCCESS },
            { 1, "", APR_EAGAIN },
            { 1, LF, APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions,
                                           sizeof(actions)/sizeof(actions[0]),
                                           alloc);
        read_and_check_bucket(tc, mock_bkt,
                              "HTTP/1.1 200 OK" CRLF);

        serf_bucket_destroy(mock_bkt);
        mock_bkt = serf_bucket_mock_create(actions,
                                           sizeof(actions)/sizeof(actions[0]),
                                           alloc);
        readlines_and_check_bucket(tc, mock_bkt, SERF_NEWLINE_CRLF,
                                   "HTTP/1.1 200 OK" CRLF, 1);
        serf_bucket_destroy(mock_bkt);
    }
    /* test more_data_arrived */
    {
        apr_status_t status;
        const char *data;
        apr_size_t len;
        int i;

        mockbkt_action actions[]= {
            { 1, "", APR_EAGAIN },
            { 1, "blabla", APR_EOF },
        };
        mock_bkt = serf_bucket_mock_create(actions,
                                           sizeof(actions)/sizeof(actions[0]),
                                           alloc);

        for (i = 0; i < 5; i++) {
            status = serf_bucket_peek(mock_bkt, &data, &len);
            CuAssertIntEquals(tc, APR_SUCCESS, status);
            CuAssertIntEquals(tc, 0, len);
            CuAssertIntEquals(tc, '\0', *data);
        }

        serf_bucket_mock_more_data_arrived(mock_bkt);

        status = serf_bucket_peek(mock_bkt, &data, &len);
        CuAssertIntEquals(tc, APR_EOF, status);
        CuAssertIntEquals(tc, 6, len);
        CuAssert(tc, "Read data is not equal to expected.",
                 strncmp("blabla", data, len) == 0);
        serf_bucket_destroy(mock_bkt);
    }
}

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

    CuSuiteSetSetupTeardownCallbacks(suite, test_setup, test_teardown);

    SUITE_ADD_TEST(suite, test_basic_mock_bucket);

    return suite;
}
