/**
 * @file format_verbs.c
 * @brief Tests the group D verbs: PRINT USING, PUDEF, WIDTH and CHAR.
 *
 * Field formatting has more edge cases than a verb usually does, and
 * akbasic_format_using() is a pure function of a format string and a value, so
 * most of this exercises it directly rather than through a program. What goes
 * through a program is the part a program can get wrong: the parse of
 * `PRINT USING fmt; value`, and the verbs that only set state.
 */

#include <string.h>

#include <akbasic/error.h>
#include <akbasic/format.h>
#include <akbasic/runtime.h>

#include "harness.h"
#include "testutil.h"

static akbasic_FormatState FORMAT;

/** @brief Run a program to completion in RUN mode, from a string. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(NULL));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/** @brief Format one value, for the direct tests below. */
static akerr_ErrorContext AKERR_NOIGNORE *fmt_number(const char *format, double number, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    akbasic_Value value;

    PASS(errctx, akbasic_value_zero(&value));
    value.valuetype = AKBASIC_TYPE_FLOAT;
    value.floatval = number;
    PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
    SUCCEED_RETURN(errctx);
}

/** @brief Format one string, for the direct tests below. */
static akerr_ErrorContext AKERR_NOIGNORE *fmt_string(const char *format, const char *text, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    akbasic_Value value;

    PASS(errctx, akbasic_value_zero(&value));
    value.valuetype = AKBASIC_TYPE_STRING;
    snprintf(value.stringval, sizeof(value.stringval), "%s", text);
    PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
    SUCCEED_RETURN(errctx);
}

/** @brief Digit positions right-justify, and the decimal places are fixed. */
static void test_numeric_fields(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));

    TEST_REQUIRE_OK(fmt_number("###.##", 3.14159, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "  3.14");

    /* Rounded to the field, not truncated. */
    TEST_REQUIRE_OK(fmt_number("###.#", 2.06, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "  2.1");

    /* No decimal point in the field means no fractional part printed. */
    TEST_REQUIRE_OK(fmt_number("####", 42.9, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "  43");

    /* Zero fills the whole field with blanks but the one digit. */
    TEST_REQUIRE_OK(fmt_number("####", 0.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "   0");
}

/** @brief Literal text around the field is copied through. */
static void test_literal_text(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(fmt_number("TOTAL: ###.## EACH", 12.5, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "TOTAL:  12.50 EACH");
}

/** @brief A comma in the field groups the integer part in threes. */
static void test_grouping(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(fmt_number("#,###,###", 1234567.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "1,234,567");

    /* Padding counts the separators, so a column stays a column. */
    TEST_REQUIRE_OK(fmt_number("#,###,###", 12.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "       12");
}

/** @brief A currency sign and a sign position are part of the field. */
static void test_decorations(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "$1,234.50");

    TEST_REQUIRE_OK(fmt_number("+####", 42.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "+  42");

    TEST_REQUIRE_OK(fmt_number("+####", -42.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "-  42");

    /* A trailing sign position puts the sign after the digits. */
    TEST_REQUIRE_OK(fmt_number("####-", -42.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "  42-");
}

/**
 * @brief A value too wide for its field fills the field with stars.
 *
 * BASIC 7.0's answer, and deliberately loud: printing more digits than the field
 * asked for would push every later column out of line, which is exactly what
 * PRINT USING is for avoiding.
 */
static void test_overflow(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(fmt_number("###", 99999.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "***");

    /*
     * A negative number in a field with no sign position overflows too. Dropping
     * the minus would print -5 as 5, which is worse than a row of stars.
     */
    TEST_REQUIRE_OK(fmt_number("###", -5.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "***");
}

/** @brief `=` centres a string in its field and `>` right-justifies it. */
static void test_string_fields(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(fmt_string("==========", "MID", out, sizeof(out)));
    TEST_REQUIRE_STR(out, "   MID    ");

    TEST_REQUIRE_OK(fmt_string(">>>>>>>>>>", "RIGHT", out, sizeof(out)));
    TEST_REQUIRE_STR(out, "     RIGHT");

    /* A string longer than its field is cut, not starred. */
    TEST_REQUIRE_OK(fmt_string("===", "TOOLONG", out, sizeof(out)));
    TEST_REQUIRE_STR(out, "TOO");
}

/** @brief A field and a value of the wrong kind for each other are refused. */
static void test_type_mismatch(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_STATUS(fmt_string("###", "TEXT", out, sizeof(out)), AKBASIC_ERR_TYPE);
    TEST_REQUIRE_STATUS(fmt_number("===", 1.0, out, sizeof(out)), AKBASIC_ERR_TYPE);

    /* A format with no field at all is a mistake rather than a literal. */
    TEST_REQUIRE_STATUS(fmt_number("NO FIELD HERE", 1.0, out, sizeof(out)), AKBASIC_ERR_SYNTAX);
}

/** @brief PUDEF replaces the fill characters, as many as it was given. */
static void test_pudef(void)
{
    char out[256];

    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "*"));
    TEST_REQUIRE_OK(fmt_number("#####", 42.0, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "***42");

    /* The other three are untouched by a one-character PUDEF. */
    TEST_REQUIRE_OK(fmt_number("#,###.##", 1234.5, out, sizeof(out)));
    TEST_REQUIRE_STR(out, "1,234.50");

    /* All four at once. */
    TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
    TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "_. ,"));
    TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
    TEST_REQUIRE_STR(out, ",1.234 50");
}

/** @brief PRINT USING parses and prints through a running program. */
static void test_print_using(void)
{
    TEST_REQUIRE_OK(run_program("10 PRINT USING \"###.##\"; 3.14159\n"
				"20 PRINT USING \"TOTAL: $#,###.##\"; 1234.5\n"
				"30 PRINT USING \"###\"; 99999\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "  3.14\nTOTAL: $1,234.50\n***\n");
    harness_stop();

    /* A plain PRINT still works, which the shared parse handler could break. */
    TEST_REQUIRE_OK(run_program("10 PRINT \"PLAIN\"\n20 PRINT 1 + 2\n30 PRINT\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "PLAIN\n3\n\n");
    harness_stop();

    /* And PUDEF reaches PRINT USING through the runtime's own state. */
    TEST_REQUIRE_OK(run_program("10 PUDEF \"*\"\n"
				"20 PRINT USING \"#####\"; 42\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "***42\n");
    harness_stop();

    /* The semicolon is required; without it the format has no value to print. */
    TEST_REQUIRE_OK(run_program("10 PRINT USING \"###\" 42\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "semicolon") != NULL,
		 "a missing semicolon should say so, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief WIDTH takes 1 or 2 and refuses anything else. */
static void test_width(void)
{
    TEST_REQUIRE_OK(run_program("10 WIDTH 2\n20 PRINT \"OK\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.linewidth, 2);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 WIDTH 3\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "WIDTH is 1 or 2") != NULL,
		 "WIDTH 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /* No device needed: it is drawing state, like COLOR's bindings. */
    TEST_REQUIRE_OK(run_program("10 WIDTH 2\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "device") == NULL,
		 "WIDTH should not need a device, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief CHAR refuses against a sink with no cursor, which is the stdio one. */
static void test_char_needs_a_cursor(void)
{
    TEST_REQUIRE_OK(run_program("10 CHAR 1, 5, 3, \"HI\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "position its cursor") != NULL,
		 "CHAR against a stdio sink should refuse by name, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();
}

int main(void)
{
    test_numeric_fields();
    test_literal_text();
    test_grouping();
    test_decorations();
    test_overflow();
    test_string_fields();
    test_type_mismatch();
    test_pudef();
    test_print_using();
    test_width();
    test_char_needs_a_cursor();
    return akbasic_test_failures;
}
