#include "../unity.h"
#include "lcut/lcut.h"

/* This file contains some unit tests which don't naturally fit into
 * the main suite of tests represented by runtests.lex and ../../grammar/testcases.csv.
 */

#define NOTNULL(tc, p) do { \
    LCUT_TRUE(tc, (ue != NULL));  if (ue == NULL) return; \
    } while (0)

static const UnitExpression* parse_expression(const char* str)
{
    const UnitExpression* tempue = unity_parse_string(str, UNITY_SYNTAX_FITS);
    if (tempue == NULL) {
        fprintf(stderr, "!!! Invalid test case: can't parse %s\n", str);
        exit(1);
    }
    return tempue;
}

void tc_basic_functionality(lcut_tc_t *tc, void *data)
{
    const Unit* u;
    const UnitDef* ud;
    const FunctionDef* fd;

    const UnitExpression* ue;

    LCUT_INT_EQUAL(tc, UNITY_SYNTAX_FITS, unity_identify_parser("fits"));
    LCUT_STR_EQUAL(tc, "fits", unity_get_syntax_name(UNITY_SYNTAX_FITS));

    ue = parse_expression("km.Mg/s");
    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_ALL));

    u = unity_get_unit_by_index(ue, 0);
    ud = unity_get_unitdef_from_unit(u);
    LCUT_STR_EQUAL(tc, "http://qudt.org/vocab/unit#Meter", unity_get_unit_uri(ud));
    LCUT_STR_EQUAL(tc, "Metre", unity_get_unit_name(ud));
    LCUT_STR_EQUAL(tc, "http://qudt.org/vocab/quantity#Length", unity_get_unit_type(ud));
    LCUT_STR_EQUAL(tc, "L", unity_get_unit_dimension(ud));
    LCUT_ASSERT(tc, "should be null", unity_get_unit_description(ud) == NULL);
    LCUT_ASSERT(tc, "should be null", unity_get_unit_latex_form(ud) == NULL);

    ud = unity_get_unitdef_from_string("s", UNITY_SYNTAX_FITS);
    LCUT_STR_EQUAL(tc, "http://qudt.org/vocab/unit#SecondTime", unity_get_unit_uri(ud));
    LCUT_STR_EQUAL(tc, "Second", unity_get_unit_name(ud));
    LCUT_STR_EQUAL(tc, "http://qudt.org/vocab/quantity#Time", unity_get_unit_type(ud));
    LCUT_STR_EQUAL(tc, "T", unity_get_unit_dimension(ud));
    LCUT_ASSERT(tc, "should be null", unity_get_unit_description(ud) == NULL);
    LCUT_ASSERT(tc, "should be null", unity_get_unit_latex_form(ud) == NULL);
    LCUT_ASSERT(tc, "should be null", unity_get_functiondef_from_unit(u) == NULL);

    unity_free_expression(ue);

    // ----

    ue = parse_expression("km/week");

    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_CONSTRAINTS));
    LCUT_TRUE(tc, !unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_RECOGNISED));
    u = unity_get_unit_by_index(ue, 1);
    ud = unity_get_unitdef_from_unit(u);
    LCUT_ASSERT(tc, "should be null", ud == NULL);
    LCUT_ASSERT(tc, "should be null", unity_get_functiondef_from_unit(u) == NULL);

    unity_free_expression(ue);

    // ----

    ue = parse_expression("log(km)/xfn(week)");

    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_CONSTRAINTS));
    LCUT_TRUE(tc, !unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_RECOGNISED));
    u = unity_get_unit_by_index(ue, 0);
    fd = unity_get_functiondef_from_unit(u);
    NOTNULL(tc, fd);
    LCUT_STR_EQUAL(tc, "Common Logarithm (base 10)", fd->description);
    LCUT_STR_EQUAL(tc, "\\log", fd->latex_form);
    LCUT_ASSERT(tc, "should be null", unity_get_unitdef_from_unit(u) == NULL);

    u = unity_get_unit_by_index(ue, 1);
    LCUT_ASSERT(tc, "should be null", unity_get_functiondef_from_unit(u) == NULL);

    fd = unity_get_functiondef_from_string("log", UNITY_SYNTAX_FITS);
    LCUT_STR_EQUAL(tc, "Common Logarithm (base 10)", fd->description);

    unity_free_expression(ue);

    // ----

    ue = parse_expression("log(km)/log(week)");

    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_CONSTRAINTS));
    LCUT_TRUE(tc, !unity_check_expression(ue, UNITY_SYNTAX_FITS, UNITY_CHECK_RECOGNISED));

    unity_free_expression(ue);
}

void tc_ordering(lcut_tc_t *tc, void *data)
{
    const char* testcases[][2] = {
        { "km.Mg/(pc.s)",		" 0: 0 6/Gram/1 3/Meter/1 0/Parsec/-1 0/SecondTime/-1" },
        { "km2/(pc2.s.Mg-1)",		" 1: 0 6/Gram/1 3/Meter/2 0/SecondTime/-1 0/Parsec/-2" },
        { "km.s", 			" 2: 0 3/Meter/1 0/SecondTime/1" },
        { "km-1.s",			" 3: 0 0/SecondTime/1 3/Meter/-1" },
        { "km.km2",			" 4: 0 3/Meter/1 3/Meter/2" },
        { "km-2.km-1",			" 5: 0 3/Meter/-1 3/Meter/-2" },
        { "boo.km",			" 6: 0 3/Meter/1 0/boo/1" },
        { "boo.wibble",			" 7: 0 0/boo/1 0/wibble/1" },
        { "log(km).km",			" 8: 0 3/Meter/1 log(3/Meter/1)" },
        { "log(wibble).km",		" 9: 0 3/Meter/1 log(0/wibble/1)" },
        { "log(km).log(s)",		"10: 0 log(3/Meter/1) log(0/SecondTime/1)" },
        { "log(km).log(boo)",		"11: 0 log(3/Meter/1) log(0/boo/1)" },
        { "log(wibble).log(boo)",	"12: 0 log(0/boo/1) log(0/wibble/1)" },
        { "xfn(km).log(km)",		"13: 0 log(3/Meter/1) xfn(3/Meter/1)" },
        { "xfn(s).xfn(km)",		"14: 0 xfn(3/Meter/1) xfn(0/SecondTime/1)"  },
        { "xfn(boo).xfn(km)",		"15: 0 xfn(3/Meter/1) xfn(0/boo/1)" },
        { "xfn(boo).xfn(wibble)",	"16: 0 xfn(0/boo/1) xfn(0/wibble/1)" },
        { NULL, NULL, },
    };
    int i;

    for (i=0; 1; i++) {
        const char *input = testcases[i][0];
        const char *expected = testcases[i][1];
        const UnitExpression* ue;
#define BUFLEN 1024
        char buffer[BUFLEN+1];

        if (input == NULL) {
            break;
        }

        ue = parse_expression(input);
        snprintf(buffer, BUFLEN, "%2d: ", i);
        unity_write_formatted(&buffer[4], BUFLEN-4, ue, UNITY_SYNTAX_DEBUG);
        LCUT_STR_EQUAL(tc, expected, buffer);
        unity_free_expression(ue);
#undef BUFLEN
    }
}

void tc_scalefactors(lcut_tc_t *tc, void *data)
{
    const UnitExpression* ue = parse_expression("km/s");
    NOTNULL(tc, ue);
    LCUT_TRUE(tc, ue->log_factor == 0.0); // scalefactor wasn't specified
    unity_free_expression(ue);

    ue = unity_parse_string("10+2 km/s", UNITY_SYNTAX_FITS);
    NOTNULL(tc, ue);
    LCUT_TRUE(tc, ue->log_factor == 2.0);
    unity_free_expression(ue);

    // the remaining tests are specific to VOUnits

    // that shouldn't parse in the VOUnits syntax
    ue = unity_parse_string("10+2 km/s", UNITY_SYNTAX_VOUNITS);
    LCUT_TRUE(tc, ue == NULL);

    // check that various things that potentially look like 'dimensionless'
    // do in fact parse as they should

    // a scalefactor which is a LIT1 token
    ue = unity_parse_string("1m", UNITY_SYNTAX_VOUNITS);
    NOTNULL(tc, ue);
    LCUT_TRUE(tc, ue->log_factor == 0.0);
    unity_free_expression(ue);

    // a scalefactor which is a LIT10 token
    ue = unity_parse_string("10m", UNITY_SYNTAX_VOUNITS);
    NOTNULL(tc, ue);
    LCUT_TRUE(tc, ue->log_factor == 1.0);
    unity_free_expression(ue);

    // a VOUNITS scalefactor
    ue = unity_parse_string("1.0m", UNITY_SYNTAX_VOUNITS);
    NOTNULL(tc, ue);
    LCUT_TRUE(tc, ue->log_factor == 0.0);
    unity_free_expression(ue);
}

void tc_equality(lcut_tc_t *tc, void *data)
{
    const UnitExpression* ue1 = parse_expression("km.s/boo");
    const UnitExpression* ue2 = parse_expression("km.s");
    const UnitExpression* ue3 = parse_expression("km.s/boo2");
    const UnitExpression* ue4 = parse_expression("10+2 km.s/boo");
    const UnitExpression* ue5 = parse_expression("boo-1.s.km1");


    LCUT_TRUE(tc, !unity_equal_expression_p(ue1, ue2));
    LCUT_TRUE(tc, !unity_equal_expression_p(ue1, ue3));
    LCUT_TRUE(tc, !unity_equal_expression_p(ue1, ue4));
    //fprintf(stderr, "ue1=%s\n", unity_write_formatted(buffer, BUFLEN, ue1, "debug"));
    //fprintf(stderr, "ue5=%s\n", unity_write_formatted(buffer, BUFLEN, ue5, "debug"));
    LCUT_TRUE(tc, unity_equal_expression_p(ue1, ue5));

    LCUT_TRUE(tc, unity_get_unit_by_index(ue1, -1) == NULL);
    LCUT_TRUE(tc, unity_get_unit_by_index(ue1, 3) == NULL);

    LCUT_TRUE(tc, unity_equal_unit_p(unity_get_unit_by_index(ue1, 0),
                                     unity_get_unit_by_index(ue5, 2)));
    LCUT_TRUE(tc, unity_equal_unit_p(unity_get_unit_by_index(ue1, 1),
                                     unity_get_unit_by_index(ue5, 1)));
    LCUT_TRUE(tc, unity_equal_unit_p(unity_get_unit_by_index(ue1, 2),
                                     unity_get_unit_by_index(ue5, 0)));
}

void tc_dimensionless(lcut_tc_t *tc, void *data)
{
    const UnitExpression* ue = unity_parse_string("1", UNITY_SYNTAX_VOUNITS);
    NOTNULL(tc, ue);

    LCUT_TRUE(tc, unity_equal_expression_p(ue, unity_get_dimensionless()));

    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_VOUNITS, UNITY_CHECK_RECOGNISED));
    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_VOUNITS, UNITY_CHECK_RECOMMENDED));
    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_VOUNITS, UNITY_CHECK_CONSTRAINTS));
    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_VOUNITS, UNITY_CHECK_ALL));

    LCUT_TRUE(tc, unity_get_unit_by_index(ue, 0) == NULL);
    char buf[10];
    unity_write_formatted(buf, 10, ue, UNITY_SYNTAX_VOUNITS);
    LCUT_STR_EQUAL(tc, "1", buf);

    unity_free_expression(ue);

    // Double-check that we can operate on the dimensionless object without surprises.
    // This should be redundant with the equality test above,
    // but although the two objects tested there are equal, they are not the same object.
    ue = unity_get_dimensionless();
    LCUT_TRUE(tc, unity_check_expression(ue, UNITY_SYNTAX_VOUNITS, UNITY_CHECK_RECOGNISED));
    LCUT_TRUE(tc, unity_get_unit_by_index(ue, 0) == NULL);
    unity_write_formatted(buf, 10, ue, UNITY_SYNTAX_VOUNITS);
    LCUT_STR_EQUAL(tc, "1", buf);
}

int main() {
    lcut_ts_t   *suite = NULL;

    LCUT_TEST_BEGIN("Unity C tests", NULL, NULL);

    LCUT_TS_INIT(suite, "Unity C tests", NULL, NULL);
    LCUT_TC_ADD(suite, "Basic functionality", tc_basic_functionality, NULL, NULL, NULL);
    LCUT_TC_ADD(suite, "Ordering", tc_ordering, NULL, NULL, NULL);
    LCUT_TC_ADD(suite, "Equality", tc_equality, NULL, NULL, NULL);
    LCUT_TC_ADD(suite, "Dimensionless", tc_dimensionless, NULL, NULL, NULL);
    LCUT_TC_ADD(suite, "Scale factors", tc_scalefactors, NULL, NULL, NULL);
    LCUT_TS_ADD(suite);

    LCUT_TEST_RUN();
    LCUT_TEST_REPORT();
    LCUT_TEST_END();

    LCUT_TEST_RESULT();
}
