#include "filedb/internal/parser_types.h"
#include "filedb/database_tables.h"

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/catch_translate_exception.hpp>

#include <tango/tango.h>

#include <sstream>

// module under test
namespace mut = FileDb::detail;

CATCH_TRANSLATE_EXCEPTION(const CORBA::Exception &ex)
{
    std::stringstream ss;
    Tango::Except::print_exception(ex, ss);
    return ss.str();
}

mut::Token next_token(mut::Tokenizer &tokenizer, std::stringstream &in)
{
    std::optional<mut::Token> maybe_token = std::nullopt;
    while(!maybe_token)
    {
        char c;
        in.get(c);
        if(in.eof())
        {
            c = '\0';
        }
        maybe_token = tokenizer.feed(c);
    }
    return *maybe_token;
}

SCENARIO("We can tokenize strings")
{
    GIVEN("A tokenizer")
    {
        using Kind = mut::Token::Kind;

        mut::Tokenizer tokenizer;

        WHEN("Fed some random words")
        {
            std::stringstream buffer{"some random words"};

            THEN("STRING tokens are emitted")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "some");

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "random");

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "words");

                AND_THEN("END_OF_FILE tokens are emitted repeatedly")
                {
                    token = next_token(tokenizer, buffer);
                    REQUIRE(token.kind == Kind::END_OF_FILE);

                    token = next_token(tokenizer, buffer);
                    REQUIRE(token.kind == Kind::END_OF_FILE);
                }
            }
        }

        WHEN("Fed some control sequences")
        {
            std::stringstream buffer{"/:,->"};

            THEN("Control tokens are emitted")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::SLASH);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::COLON);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::COMMA);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::ARROW);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::END_OF_FILE);
            }
        }

        WHEN("Fed two words with an escaped new line separating them")
        {
            std::stringstream buffer{"two \\\n words"};

            THEN("Only STRING tokens are emitted")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "two");

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "words");
            }
        }

        WHEN("Fed two words in quotes")
        {
            std::stringstream buffer{"\"two/:.->words\""};

            THEN("Only one STRING token is emitted")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "two/:.->words");

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::END_OF_FILE);
            }
        }

        WHEN("Fed a new line in quotes")
        {
            std::stringstream buffer{"\"two\n\""};

            THEN("An ERROR token is emitted repeatedly")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::ERROR);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::ERROR);
            }
        }

        WHEN("Fed unterminated quotes")
        {
            std::stringstream buffer{"\"two"};

            THEN("An ERROR token is emitted repeatedly")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::ERROR);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::ERROR);
            }
        }

        WHEN("Fed a line starting with a #")
        {
            std::stringstream buffer{"# A comment\ntwo words"};

            THEN("The comment is ignored")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "two");

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "words");
            }
        }

        WHEN("Fed a multi-line string")
        {
            std::stringstream buffer{"# A comment\ntwo\\\nwords\n/more words"};
            THEN("The line and column numbers are correct")
            {
                auto token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "two");
                REQUIRE(token.col == 1);
                REQUIRE(token.lineno == 2);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "words");
                REQUIRE(token.col == 1);
                REQUIRE(token.lineno == 3);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::SLASH);
                REQUIRE(token.col == 1);
                REQUIRE(token.lineno == 4);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "more");
                REQUIRE(token.col == 2);
                REQUIRE(token.lineno == 4);

                token = next_token(tokenizer, buffer);
                REQUIRE(token.kind == Kind::STRING);
                REQUIRE(token.str == "words");
                REQUIRE(token.col == 7);
                REQUIRE(token.lineno == 4);
            }
        }

        WHEN("We set unquoted_is_string")
        {
            tokenizer.unquoted_is_string();
            AND_WHEN("We feed in a word with a '/'")
            {
                std::stringstream buffer{"two/words"};
                THEN("A STRING token is emitted including the '/'")
                {
                    auto token = next_token(tokenizer, buffer);
                    REQUIRE(token.kind == Kind::STRING);
                    REQUIRE(token.str == "two/words");
                }
            }
        }
    }
}

SCENARIO("We can parse records")
{
    GIVEN("A parser fed a string containing a server record")
    {
        std::stringstream buffer{"executable/instance/DEVICE/class: domain1/family1/member1, domain2/family2/member2"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a ServerRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::ServerRecord>(event);
                REQUIRE(record.device_class() == "class");
                REQUIRE(record.server() == "executable/instance");
                REQUIRE_THAT(record.devices,
                             Equals(std::vector<std::string>{"domain1/family1/member1", "domain2/family2/member2"}));
            }
        }
    }

    GIVEN("A parser fed a string containing a class property record")
    {
        std::stringstream buffer{"CLASS/class->prop: value1, value2, value3"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a ClassPropertyRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::ClassPropertyRecord>(event);
                REQUIRE(record.device_class() == "class");
                REQUIRE(record.property() == "prop");
                REQUIRE_THAT(record.values, Equals(std::vector<std::string>{"value1", "value2", "value3"}));
            }
        }
    }

    GIVEN("A parser fed a string containing a free object property record")
    {
        std::stringstream buffer{"FREE/object->prop: value1, value2, value3"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a FreeObjectPropertyRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::FreeObjectPropertyRecord>(event);
                REQUIRE(record.object() == "object");
                REQUIRE(record.property() == "prop");
                REQUIRE_THAT(record.values, Equals(std::vector<std::string>{"value1", "value2", "value3"}));
            }
        }
    }

    GIVEN("A parser fed a string containing a device property record")
    {
        std::stringstream buffer{"domain/family/member->prop: value1, value2, value3"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a DevicePropertyRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::DevicePropertyRecord>(event);
                REQUIRE(record.device() == "domain/family/member");
                REQUIRE(record.property() == "prop");
                REQUIRE_THAT(record.values, Equals(std::vector<std::string>{"value1", "value2", "value3"}));
            }
        }
    }

    GIVEN("A parser fed a string containing a device attribute property record")
    {
        std::stringstream buffer{"domain/family/member/attr->prop: value1, value2, value3"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a DeviceAttributePropertyRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::DeviceAttributePropertyRecord>(event);
                REQUIRE(record.device() == "domain/family/member");
                REQUIRE(record.attribute() == "attr");
                REQUIRE(record.property() == "prop");
                REQUIRE_THAT(record.values, Equals(std::vector<std::string>{"value1", "value2", "value3"}));
            }
        }
    }

    GIVEN("A parser fed a string containing a class attribute property record")
    {
        std::stringstream buffer{"CLASS/device_class/attr->prop: value1, value2, value3"};

        mut::Parser parser{buffer};

        WHEN("We get the first record")
        {
            THEN("We recieve a ClassAttributePropertyRecord event")
            {
                using Catch::Matchers::Equals;

                mut::Parser::RecordEvent event;
                REQUIRE_NOTHROW(event = parser.next_event());

                auto record = std::get<FileDb::ClassAttributePropertyRecord>(event);
                REQUIRE(record.device_class() == "device_class");
                REQUIRE(record.attribute() == "attr");
                REQUIRE(record.property() == "prop");
                REQUIRE_THAT(record.values, Equals(std::vector<std::string>{"value1", "value2", "value3"}));
            }
        }
    }
}
