/*
**  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_file_io.h"
#include "at.h"
#include "apr_lib.h"
#include "apr_strings.h"
#include "apr_tables.h"
#include "apr_env.h"
#include <assert.h>


apr_status_t at_begin(at_t *t, int total)
{
    char buf[32];
    apr_snprintf(buf, 32, "1..%d", total);
    return at_report(t, buf);
}

static apr_status_t test_cleanup(void *data)
{
    at_t *t = data;
    if (t->current < t->plan)
        return at_report(t, "Bail out!");
    else
        return at_report(t, "END");
}

void at_end(at_t *t)
{
    apr_pool_cleanup_kill(t->pool, t, test_cleanup);
    test_cleanup(t);
}

apr_status_t at_comment(at_t *t, const char *fmt, va_list vp)
{
    apr_status_t s;
    char buf[256], *b = buf + 2;
    char *end;
    int rv;
    rv = apr_vsnprintf(b, 250, fmt, vp);

    if (rv <= 0)
        return APR_EINVAL;


    end = b + rv;

    buf[0] = '#';
    buf[1] = ' ';

    if (rv == 250) {
        end[-1] = '.';
        *end++ = '.';
        *end++ = '.';
        *end++ = '\n';
        *end = 0;
    }
    else if (end[-1] != '\n') {
        *end++ = '\n';
        *end = 0;
    }

    b = buf;
    while (1) {
        char *eol;

        eol = strchr(b + 2, '\n');
        *eol = 0;
        s = at_report(t, b);
        if (s != APR_SUCCESS || eol == end - 1)
            break;

        b    = eol - 1;
        b[0] = '#';
        b[1] = ' ';
    }

    return s;
}

void at_ok(at_t *t, int is_ok, const char *label, const char *file, int line)
{
    char format[] = "not ok %d - %s # %s (%s:%d) test %d in %s";
    char *end = format + 10;
    char *fmt = is_ok ? format + 4 : format;
    char buf[256];
    const char *comment = NULL;
    int rv, is_fatal = 0, is_skip = 0, is_todo = 0;

    t->current++;

    if (*t->fatal == t->current) {
        t->fatal++;
        is_fatal = 1;
    }

    if (*t->skip == t->current) {
        t->skip++;
        is_skip = 1;
    }

    if (*t->todo == t->current) {
        t->todo++;
        is_todo = 1;
    }

    if (AT_FLAG_CONCISE(t->flags))
        format[9] = '\0';
    else if (is_ok && !AT_FLAG_TRACE(t->flags))
        format[14] = '\0';
    else if (is_fatal && ! is_ok)
        comment = "fatal";
    else
        comment = is_todo ? "todo" : is_skip ? "skip" : "at";

    rv = apr_snprintf(buf, 256, fmt, t->current + t->prior,
                      label, comment,  file, line, t->current, t->name);

    if (rv <= 0)
        exit(-1);

    end = buf + rv;

    if (rv == 250) {
        *end++ = '.';
        *end++ = '.';
        *end++ = '.';
        *end = '\0';
    }

    if (memchr(buf, '\n', rv) != NULL || at_report(t, buf) != APR_SUCCESS)
        exit(-1);

    if (!is_ok && is_fatal) {
        while (t->current++ < t->plan) {
            apr_snprintf(buf, 256, "not ok %d # skipped: aborting test %s",
                     t->prior + t->current, t->name);
            at_report(t, buf);
        }
        longjmp(*t->abort, 0);
    }
}

struct at_report_file {
    at_report_t module;
    apr_file_t *file;
};


static apr_status_t at_report_file_write(at_report_t *ctx, const char *msg)
{
    struct at_report_file *r = (struct at_report_file *)ctx;
    apr_file_t *f = r->file;
    apr_size_t len = strlen(msg);
    apr_status_t s;

    s = apr_file_write_full(f, msg, len, &len);
    if (s != APR_SUCCESS)
        return s;

    s = apr_file_putc('\n', f);
    if (s != APR_SUCCESS)
        return s;

    return apr_file_flush(f);
}

at_report_t *at_report_file_make(apr_pool_t *p, apr_file_t *f)
{
    struct at_report_file *r = apr_palloc(p, sizeof *r);
    r->module.func = at_report_file_write;
    r->file = f;
    return &r->module;
}



struct at_report_local {
    at_report_t  module;
    at_t        *t;
    at_report_t *saved_report;
    const int   *saved_fatal;
    int          dummy_fatal;
    const char  *file;
    int          line;
    int          passed;
    apr_pool_t  *pool;
};


static apr_status_t report_local_cleanup(void *data)
{
    struct at_report_local *q = data;
    dAT = q->t;
    char label[32];

    apr_snprintf(label, 32, "collected %d passing tests", q->passed);

    AT->report = q->saved_report;
    AT->fatal = q->saved_fatal;

    at_ok(q->t, 1, label, q->file, q->line);
    return APR_SUCCESS;
}


static apr_status_t at_report_local_write(at_report_t *ctx, const char *msg)
{
    char buf[256];
    struct at_report_local *q = (struct at_report_local *)ctx;
    dAT = q->t;

    if (strncmp(msg, "not ok", 6) == 0) {
        q->saved_report->func(q->saved_report, msg);
        AT->report = q->saved_report;
        AT->fatal = q->saved_fatal;
        apr_pool_cleanup_kill(q->pool, q, report_local_cleanup);
        while (AT->current++ < AT->plan) {
            apr_snprintf(buf, 256, "not ok %d # skipped: aborting test %s",
                     AT->prior + AT->current, AT->name);
            at_report(AT, buf);
        }
        longjmp(*AT->abort, 0);
    }
    else if (strncmp(msg, "ok", 2) == 0) {
        AT->current--;
        q->passed++;
    }
    return APR_SUCCESS;
}

void at_report_local(at_t *AT, apr_pool_t *p, const char *file, int line)
{
    struct at_report_local *q = apr_palloc(p, sizeof *q);
    q->module.func = at_report_local_write;
    q->t = AT;
    q->saved_report = AT->report;
    q->saved_fatal = AT->fatal;
    q->dummy_fatal = 0;
    q->file = apr_pstrdup(p, file);
    q->line = line;
    q->passed = 0;
    q->pool = p;

    AT->fatal = &q->dummy_fatal;
    AT->report = &q->module;

    if (*q->saved_fatal == AT->current + 1)
        q->saved_fatal++;

    apr_pool_cleanup_register(p, q, report_local_cleanup,
                                    report_local_cleanup);
}


at_t *at_create(apr_pool_t *pool, unsigned char flags, at_report_t *report)
{
    at_t *t = apr_pcalloc(pool, sizeof *t);
    t->flags = flags;
    t->report = report;
    t->pool = pool;

    apr_pool_cleanup_register(pool, t, test_cleanup, test_cleanup);
    return t;
}


#define AT_NELTS 4

static int* at_list(apr_pool_t *pool, const char *spec, int *list)
{
    apr_array_header_t arr;
    int prev, current = 0;

    arr.pool = pool;
    arr.elt_size = sizeof *list;
    arr.nelts = 0;
    arr.nalloc = AT_NELTS;
    arr.elts = (char *)list;

    do {
        while (*spec && !apr_isdigit(*spec))
            ++spec;

        prev = current;
        current = (int)apr_strtoi64(spec, (char **)(void *)&spec, 10);
        *(int *)apr_array_push(&arr) = current;

    } while (prev >= current);

    return (int *)arr.elts;
}



apr_status_t at_run(at_t *AT, const at_test_t *test)
{
    int dummy = 0, fbuf[AT_NELTS], sbuf[AT_NELTS], tbuf[AT_NELTS];
    jmp_buf j;

    AT->current = 0;
    AT->prior   += AT->plan;
    AT->name    = test->name;
    AT->plan    = test->plan;

    if (test->fatals)
        AT->fatal = at_list(AT->pool, test->fatals, fbuf);
    else
        AT->fatal = &dummy;

    if (test->skips)
        AT->skip = at_list(AT->pool, test->skips, sbuf);
    else
        AT->skip = &dummy;

    if (test->todos)
        AT->todo = at_list(AT->pool, test->todos, tbuf);
    else
        AT->todo = &dummy;

    AT->abort = &j;
    if (setjmp(j) == 0) {
        test->func(AT);
        return APR_SUCCESS;
    }
    AT->abort = NULL;
    return APR_EGENERAL;
}
