#include <unity.h>
#include <Arduino.h>
#include <StreamString.h>

#include <terminal_commands.h>

// TODO: should we just use std::function at this point?
//       we don't actually benefit from having basic ptr functions in handler
//       test would be simplified too, we would no longer need to have static vars

// Got the idea from the Embedis test suite, set up a proxy for StreamString
// Real terminal processing happens with ringbuffer'ed stream
struct IOStreamString : public Stream {
    StreamString in;
    StreamString out;

    size_t write(uint8_t ch) final override {
        return in.write(ch);
    }

    int read() final override {
        return out.read();
    }

    int available() final override {
        return out.available();
    }

    int peek() final override {
        return out.peek();
    }

    void flush() final override {
        out.flush();
    }
};

// We need to make sure that our changes to split_args actually worked

void test_hex_codes() {

    static bool abc_done = false;

    terminal::Terminal::addCommand(F("abc"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL(2, ctx.argc);
        TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[0].c_str());
        TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[1].c_str());
        abc_done = true;
    });

    IOStreamString str;
    str.out += String("abc \"\x61\x62\x63\"\r\n");

    terminal::Terminal handler(str);

    TEST_ASSERT_EQUAL(
        terminal::Terminal::Result::Command,
        handler.processLine()
    );
    TEST_ASSERT(abc_done);
}

// Ensure that we can register multiple commands (at least 3, might want to test much more in the future?)
// Ensure that registered commands can be called and they are called in order

void test_multiple_commands() {

    // set up counter to be chained between commands
    static int command_calls = 0;

    terminal::Terminal::addCommand(F("test1"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
        TEST_ASSERT_EQUAL(0, command_calls);
        command_calls = 1;
    });
    terminal::Terminal::addCommand(F("test2"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
        TEST_ASSERT_EQUAL(1, command_calls);
        command_calls = 2;
    });
    terminal::Terminal::addCommand(F("test3"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
        TEST_ASSERT_EQUAL(2, command_calls);
        command_calls = 3;
    });

    IOStreamString str;
    str.out += String("test1\r\ntest2\r\ntest3\r\n");

    terminal::Terminal handler(str);

    // each processing step only executes a single command
    static int process_counter = 0;

    handler.process([](terminal::Terminal::Result result) -> bool {
        if (process_counter == 3) {
            TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::NoInput);
            return false;
        } else {
            TEST_ASSERT_EQUAL(result, terminal::Terminal::Result::Command);
            ++process_counter;
            return true;
        }
        TEST_FAIL_MESSAGE("Should not be reached");
        return false;
    });
    TEST_ASSERT_EQUAL(3, command_calls);
    TEST_ASSERT_EQUAL(3, process_counter);

}

void test_command() {

    static int counter = 0;

    terminal::Terminal::addCommand(F("test.command"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argc, "Command without args should have argc == 1");
        ++counter;
    });

    IOStreamString str;
    terminal::Terminal handler(str);

    TEST_ASSERT_EQUAL_MESSAGE(
        terminal::Terminal::Result::NoInput, handler.processLine(),
        "We have not read anything yet"
    );

    str.out += String("test.command\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT_EQUAL_MESSAGE(1, counter, "At this time `test.command` was called just once");

    str.out += String("test.command");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Pending, handler.processLine());
    TEST_ASSERT_EQUAL_MESSAGE(1, counter, "We are waiting for either \\r\\n or \\n, handler still has data buffered");

    str.out += String("\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT_EQUAL_MESSAGE(2, counter, "We should call `test.command` the second time");

    str.out += String("test.command\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT_EQUAL_MESSAGE(3, counter, "We should call `test.command` the third time, with just LF");

}

// Ensure that we can properly handle arguments

void test_command_args() {

    static bool waiting = false;

    terminal::Terminal::addCommand(F("test.command.arg1"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL(2, ctx.argc);
        waiting = false;
    });

    terminal::Terminal::addCommand(F("test.command.arg1_empty"), [](const terminal::CommandContext& ctx) {
        TEST_ASSERT_EQUAL(2, ctx.argc);
        TEST_ASSERT(!ctx.argv[1].length());
        waiting = false;
    });

    IOStreamString str;
    terminal::Terminal handler(str);

    waiting = true;
    str.out += String("test.command.arg1 test\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT(!waiting);

    waiting = true;
    str.out += String("test.command.arg1_empty \"\"\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT(!waiting);

}

// Ensure that we return error when nothing was handled, but we kept feeding the processLine() with data

void test_buffer() {

    IOStreamString str;
    str.out += String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");

    terminal::Terminal handler(str, str.out.available() - 8);
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::BufferOverflow, handler.processLine());

}

// sdssplitargs returns nullptr when quotes are not terminated and empty char for an empty string. we treat it all the same

void test_quotes() {

    terminal::Terminal::addCommand(F("test.quotes"), [](const terminal::CommandContext& ctx) {
        for (auto& arg : ctx.argv) {
            TEST_MESSAGE(arg.c_str());
        }
        TEST_FAIL_MESSAGE("`test.quotes` should not be called");
    });

    IOStreamString str;
    terminal::Terminal handler(str);

    str.out += String("test.quotes \"quote without a pair\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());

    str.out += String("test.quotes 'quote without a pair\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::NoInput, handler.processLine());

}

// we specify that commands lowercase == UPPERCASE, both with hashed values and with equality functions
// (internal note: we use std::unordered_map at this time)

void test_case_insensitive() {

    terminal::Terminal::addCommand(F("test.lowercase1"), [](const terminal::CommandContext& ctx) {
        TEST_FAIL_MESSAGE("`test.lowercase1` was registered first, but there's another function by the same name. This should not be called");
    });
    terminal::Terminal::addCommand(F("TEST.LOWERCASE1"), [](const terminal::CommandContext& ctx) {
        __asm__ volatile ("nop");
    });

    IOStreamString str;
    terminal::Terminal handler(str);

    str.out += String("TeSt.lOwErCaSe1\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());

}

// We can use command ctx.output to send something back into the stream

void test_output() {

    terminal::Terminal::addCommand(F("test.output"), [](const terminal::CommandContext& ctx) {
        if (ctx.argc != 2) return;
        ctx.output.print(ctx.argv[1]);
    });

    IOStreamString str;
    terminal::Terminal handler(str);

    char match[] = "test1234567890";
    str.out += String("test.output ") + String(match) + String("\r\n");
    TEST_ASSERT_EQUAL(terminal::Terminal::Result::Command, handler.processLine());
    TEST_ASSERT_EQUAL_STRING(match, str.in.c_str());

}

// When adding test functions, don't forget to add RUN_TEST(...) in the main()

int main(int argc, char** argv) {
    UNITY_BEGIN();
    RUN_TEST(test_command);
    RUN_TEST(test_command_args);
    RUN_TEST(test_multiple_commands);
    RUN_TEST(test_hex_codes);
    RUN_TEST(test_buffer);
    RUN_TEST(test_quotes);
    RUN_TEST(test_case_insensitive);
    RUN_TEST(test_output);
    UNITY_END();
}
