#include "json2/event.hpp"
#include "json2/except.hpp"
#include "tap.hpp"

#include <vector>
#include <cstring>
#include <functional>


///////////////////////////////////////////////////////////////////////////////
void
test_numbers (util::TAP::logger &tap)
{
    static const struct {
        const char *data;
        bool good;
        const char *message;
    } TESTS[] = {
        { "1", true, "single digit" },
        { "01", false, "leading zero" },
        { "-1", true, "leading minus" },
        { "+1", false, "leading plus" },
        { "1.", false, "truncated fraction" },
        { "1.0", true, "fraction" },
        { "1.0e", false, "truncated exponential" },
        { "1.0e1", true, "lower exponential" },
        { "1.0E1", true, "upper exponential" },
        { "1.0e+1", true, "positive exponential" },
        { "1.0e+", false, "truncated positive exponential" },
        { "1.0e-1", true, "negative exponential" },
        { "1.0e-", false, "truncated negative exponential" },
    };

    for (const auto &t: TESTS) {
        auto first = t.data;
        auto last  = first + strlen (first);

        if (!t.good) {
            tap.expect_throw<util::json2::error> (
                [&] () {
                    util::json2::event::parse ([] (auto) {}, first, last);
                },
                "number, %s",
                t.message
            );
        } else {
            util::json2::event::type_t type;
            auto end = util::json2::event::parse (
                [&type] (auto p) { type = p.type (); }, first, last
            );

            tap.expect (
                last == end && type == util::json2::event::type_t::NUMBER,
                "number, %s",
                t.message
            );
        }
    }
}


///////////////////////////////////////////////////////////////////////////////
void
test_literals (util::TAP::logger &tap)
{
    static const struct {
        const char *data;
        bool good;
        util::json2::event::type_t type;
        const char *message;
    } TESTS[] = {
        { "true", true,  util::json2::event::type_t::BOOLEAN, "lower true" },
        { "TRUE", false, util::json2::event::type_t::BOOLEAN, "upper true" },
        { "truE", false, util::json2::event::type_t::BOOLEAN, "mixed true" },
        { "tru",  false, util::json2::event::type_t::BOOLEAN, "truncated true" },

        { "false", true, util::json2::event::type_t::BOOLEAN, "lower false" },
        { "FALSE", false, util::json2::event::type_t::BOOLEAN, "upper false" },
        { "falSe", false, util::json2::event::type_t::BOOLEAN, "mixed false" },
        { "fals",  false, util::json2::event::type_t::BOOLEAN, "truncated false" },

        { "null", true, util::json2::event::type_t::NONE, "lower null" },
        { "NULL", false, util::json2::event::type_t::NONE, "upper null" },
        { "nUll", false, util::json2::event::type_t::NONE, "mixed null" },
        { "nul",  false, util::json2::event::type_t::NONE, "truncated null" },
    };

    for (const auto &t: TESTS) {
        auto first = t.data;
        auto last  = first + strlen (first);

        if (t.good) {
            util::json2::event::type_t type;
            util::json2::event::parse ([&type] (auto p) { type = p.type (); }, first, last);
            tap.expect_eq (type, t.type, "literal, %s", t.message);
        } else {
            tap.expect_throw<util::json2::parse_error> (
                [first,last] () {
                    util::json2::event::parse ([] (auto) {}, first, last);
                },
                "literal, %s",
                t.message
            );
        }
    }
}


///////////////////////////////////////////////////////////////////////////////
void
test_strings (util::TAP::logger &tap)
{
    static const struct {
        const char *data;
        bool good;
        const char *message;
    } TESTS[] = {
        { "\"abc\"", true, "abc" },
        { "\"\"", true, "empty" },
        { "\"", false, "unbalanced quote" },
        { "\"\\n\"", true, "newline escape" },
        { "\"\\a\"", true, "valid unnecessary escape" },
        { "\"\\uABCD\"", true, "upper unicode hex escape" },
        { "\"\\uabcd\"", true, "lower unicode hex escape" },
        { "\"\\uab\"", false, "truncated unicode hex escape" },
        { "\"\\uabxy\"", false, "invalid unicode hex escape" },
    };

    for (const auto &t: TESTS) {
        auto first = t.data;
        auto last  = first + strlen (first);

        if (t.good) {
            util::json2::event::type_t type;
            util::json2::event::parse ([&type] (auto p) { type = p.type (); }, first, last);
            tap.expect_eq (type, util::json2::event::type_t::STRING, "string, %s", t.message);
        } else {
            tap.expect_throw<util::json2::parse_error> (
                [first,last] () {
                    util::json2::event::parse ([] (auto) {}, first, last);
                },
                "string, %s",
                t.message
            );
        }
    }
};


///////////////////////////////////////////////////////////////////////////////
void
test_arrays (util::TAP::logger &tap)
{
    using util::json2::event::type_t;

    static const struct {
        const char *data;
        bool good;
        std::vector<util::json2::event::type_t> types;
        const char *message;
    } TESTS[] = {
        {
            "[]",
            true,
            {
                type_t::ARRAY_BEGIN,
                type_t::ARRAY_END
            },
            "empty"
        },
        {
            "[1]",
            true,
            {
                type_t::ARRAY_BEGIN,
                type_t::NUMBER,
                type_t::ARRAY_END
            },
            "single number"
        },
        {
            "[1true]",
            false,
            {
                type_t::ARRAY_BEGIN,
                type_t::NUMBER
            },
            "contatenated number/bool"
        },
        {
            "[1,2]",
            true,
            {
                type_t::ARRAY_BEGIN,
                type_t::NUMBER,
                type_t::NUMBER,
                type_t::ARRAY_END
            },
            "two numbers"
        },
        {
            "[1,]",
            false,
            {
                type_t::ARRAY_BEGIN,
                type_t::NUMBER
            },
            "single trailing comma"
        },
        {
            "[1,2,]",
            false,
            {
                type_t::ARRAY_BEGIN,
                type_t::NUMBER,
                type_t::NUMBER
            },
            "double trailing comma"
        },
        {
            "[,]",
            false,
            {
                type_t::ARRAY_BEGIN
            },
            "only comma"
        },
        {
            "[",
            false,
            {
                type_t::ARRAY_BEGIN
            },
            "missing terminator"
        },
        {
            "[[]]",
            true,
            {
                type_t::ARRAY_BEGIN,
                type_t::ARRAY_BEGIN,
                type_t::ARRAY_END,
                type_t::ARRAY_END
            },
            "nested array"
        },
        {
            "[[]",
            false,
            {
                type_t::ARRAY_BEGIN,
                type_t::ARRAY_END
            },
            "unbalanced nested array"
        },
    };

    for (const auto &t: TESTS) {
        auto first = t.data;
        auto last  = first + strlen (first);

        if (t.good) {
            std::vector<type_t> types;
            util::json2::event::parse ([&types] (auto p) { types.push_back (p.type ()); }, first, last);
            tap.expect_eq (types, t.types, "array, %s", t.message);
        } else {
            tap.expect_throw<util::json2::parse_error> (
                [&] () {
                    util::json2::event::parse ([] (auto) { }, first, last);
                }, "array, %s", t.message
            );
        }
    }
};


///////////////////////////////////////////////////////////////////////////////
void
test_objects (util::TAP::logger &tap)
{
    using util::json2::event::type_t;

    static const struct {
        const char *data;
        bool good;
        std::vector<type_t> types;
        std::vector<std::string> strings;
        const char *message;
    } TESTS[] = {
        {
            "{}",
            true,
            {
                type_t::OBJECT_BEGIN,
                type_t::OBJECT_END
            },
            {},
            "empty"
        },
        {
            "{",
            false,
            { type_t::OBJECT_BEGIN },
            {},
            "missing terminator"
        },
        {
            R"json({"a":1})json",
            true,
            {
                type_t::OBJECT_BEGIN,
                type_t::STRING,
                type_t::NUMBER,
                type_t::OBJECT_END
            },
            {
                "\"a\""
            },
            "empty"
        },
        {
            "{1:1}",
            false,
            {
                type_t::OBJECT_BEGIN
            },
            {},
            "integer key"
        },
        {
            "{:1}",
            false,
            {
                type_t::OBJECT_BEGIN
            },
            {},
            "no key"
        },
        {
            R"json({"a":})json",
            false,
            {
                type_t::OBJECT_BEGIN,
                type_t::STRING
            },
            {},
            "no value"
        },
        {
            R"json({"a":[]})json",
            true,
            {
                type_t::OBJECT_BEGIN,
                type_t::STRING,
                type_t::ARRAY_BEGIN,
                type_t::ARRAY_END,
                type_t::OBJECT_END
            },
            { "\"a\"" },
            "array value" },
        {
            R"json({ "a": { "b": null } })json",
            true,
            {
                type_t::OBJECT_BEGIN,
                type_t::STRING,
                type_t::OBJECT_BEGIN,
                type_t::STRING,
                type_t::NONE,
                type_t::OBJECT_END,
                type_t::OBJECT_END
            },
            {
                "\"a\"",
                "\"b\""
            },
            "recursive object"
        }
    };

    for (const auto &t: TESTS) {
        auto first = t.data;
        auto last  = first + strlen (first);

        if (t.good) {
            std::vector<type_t> types;
            std::vector<std::string> strings;

            util::json2::event::parse ([&] (auto p) {
                types.push_back (p.type ());
                if (p.type () == type_t::STRING)
                    strings.push_back (std::string {p.first, p.last});
            }, first, last);

            tap.expect (types == t.types && strings == t.strings, "object, %s", t.message);
        } else {
            tap.expect_throw<util::json2::parse_error> (
                [&] () {
                    util::json2::event::parse ([] (auto) {}, first, last);
                }, "object, %s", t.message
            );
        }
    }
};


///////////////////////////////////////////////////////////////////////////////
void
test_jsonish (util::TAP::logger &tap)
{
    static const struct {
        const char *data;
        const char *message;
    } TESTS[] = {
        //{ "{}", "empty object" },
        //{ "0xdeadbeef", "hex literal" },
        //{ "0b11011100", "binary literal" },
        //{ "0666", "octal literal" },
        //{ "0.", "float without frac" },
        //{ "+0.", "float with leading +" },
        //{ "-0.", "float with leading -" },
        //{ "string", "bare literal string" },
        //{ "{foo: 1}", "bare string key" },
        //{ "{foo: bar}", "bare string key and value" },
        //{ "{foo: bar,}", "trailing object comma" },
        //{ "[1,]", "trailing array comma" },
        //{ "1 #comment", "trailing comment" },
        { "#comment\n1", "leading comment" },
    };

    for (const auto &t: TESTS) {
        bool success = true;
        try {
            util::json2::event::parse<util::json2::personality::jsonish> (
                [] (auto) {},
                t.data,
                t.data + strlen (t.data)
            );
        } catch (const util::json2::error&) {
            success = false;
        }

        tap.expect (success, "jsonish, %s", t.message);
    }
};


///////////////////////////////////////////////////////////////////////////////
int
main (void)
{
    util::TAP::logger tap;

    test_numbers (tap);
    test_literals (tap);
    test_strings (tap);
    test_arrays (tap);
    test_objects (tap);

    test_jsonish (tap);

    return tap.status ();
}