#include "test.h"

#include "line_intelligence.h"

#include <random>

using namespace std;

TEST_CASE("tabs") {
	CHECK(LineIntelligence::make_tabs(0) == "");
	CHECK(LineIntelligence::make_tabs(1) == "    ");
	CHECK(LineIntelligence::make_tabs(2) == "        ");
}

TEST_CASE("code") {
	list<string> out;
	list<string> expected = {
		"func(){",
		"    i;",
		"    j;",
		"    while(true){",
		"        k;",
		"    }",
		"}"};

	string in = "func(){i;j;while(true){k;}}";
		LineIntelligence::process_code(in, &out);
	CHECK(out == expected);
}

TEST_CASE("code quote") {
	list<string> out;
	list<string> expected = {
		"func(){",
		"    i;",
		"    j = \"}{{{\";",
		"    while(true){",
		"        k;",
		"    }",
		"}"};

	string in = "func(){i;j = \"}{{{\";while(true){k;}}";
	LineIntelligence::process_code(in, &out);
	CHECK(out == expected);
}

TEST_CASE("json") {
	list<string> out;
	list<string> expected = {
		"{",
		"    \"key\":\"value\",",
		"    \"obj\":{",
		"        \"a\":2,",
		"        \"b\":3",
		"    }",
		"}"};

	string in = "{\"key\":\"value\", \"obj\":{\"a\":2, \"b\":3}}";
	map<string, size_t> counts;
	LineIntelligence::count_occurrences(in, &counts);
	CHECK(LineIntelligence::heuristic_json(in, counts));
	CHECK(!LineIntelligence::heuristic_code(in, counts));
	CHECK(!LineIntelligence::heuristic_newlines(in, counts));
	CHECK(!LineIntelligence::heuristic_pipe(in, counts));
	CHECK(!LineIntelligence::heuristic_httparg(in, counts));

	LineIntelligence::process_json(in, &out);
	CHECK(out == expected);
}

TEST_CASE("json array") {
	list<string> out;
	list<string> expected = {
		"[",
		"    3,",
		"    \"hello, there\",",
		"    true",
		"]"};

	string in = "[3, \"hello, there\", true]";
	LineIntelligence::process_json(in, &out);
	CHECK(out == expected);
}

template <typename T, typename R>
bool equal(const T& lhs, const R& rhs) {
	auto lit = lhs.begin();
	auto rit = rhs.begin();
	while (lit != lhs.end()) {
		if (rit == rhs.end()) return false;
		if (*lit != *rit) return false;
		++lit;
		++rit;
	}
	if (rit != rhs.end()) return false;
	return true;
}

TEST_CASE("split with empty") {
	string data = "one,two,,four";
	list<string> expected = {"one", "two", "", "four"};
	list<string_view> tokens;
	LineIntelligence::split_with_empty(data, ",", &tokens);
	CHECK(equal(tokens, expected));
}

TEST_CASE("split with empty end") {
	string data = "one,two,,";
	list<string> expected = {"one", "two", "", ""};
	list<string_view> tokens;
	LineIntelligence::split_with_empty(data, ",", &tokens);
	CHECK(equal(tokens, expected));
}

TEST_CASE("split with empty start") {
	string data = "-one-two";
	list<string> expected = {"", "one", "two"};
	list<string_view> tokens;
	LineIntelligence::split_with_empty(data, "-", &tokens);
	CHECK(equal(tokens, expected));
}

TEST_CASE("no split") {
	string data = "one-two";
	list<string> expected = {"one-two"};
	list<string_view> tokens;
	LineIntelligence::split_with_empty(data, ".", &tokens);
	CHECK(equal(tokens, expected));
}

TEST_CASE("empty split") {
	string data = "";
	list<string> expected = {""};
	list<string_view> tokens;
	LineIntelligence::split_with_empty(data, ",", &tokens);
	CHECK(equal(tokens, expected));
}

TEST_CASE("replace multi") {
	string data = "hello, there, world";
	CHECK(LineIntelligence::replace(data, ", ", ".")
	      == "hello.there.world");
}

TEST_CASE("replace char") {
	string data = "hello there";
	CHECK(LineIntelligence::replace(data, "t", ".")
	      == "hello .here");
}

TEST_CASE("replace none") {
	string data = "hello there";
	CHECK(LineIntelligence::replace(data, "why", "what")
	      == data);
}

TEST_CASE("rewind") {
	string data = "hello there wherecanwefind a good break?";
	CHECK(LineIntelligence::rewind(data, 4, 2) == 4);
	CHECK(LineIntelligence::rewind(data, 7, 5) == 5);
	CHECK(LineIntelligence::rewind(data, 24, 2) == 24);
	CHECK(LineIntelligence::rewind(data, 24, 15) == 11);
}

TEST_CASE("rewind punctuation") {
	string data = "a=b&c=d";
	CHECK(LineIntelligence::rewind(data, 2, 1) == 2);
	CHECK(LineIntelligence::rewind(data, 3, 2) == 3);
	CHECK(LineIntelligence::rewind(data, 4, 1) == 4);
	CHECK(LineIntelligence::rewind(data, 4, 2) == 3);
	CHECK(LineIntelligence::rewind(data, 4, 3) == 3);
	CHECK(LineIntelligence::rewind(data, 5, 1) == 5);
	CHECK(LineIntelligence::rewind(data, 5, 2) == 5);
	CHECK(LineIntelligence::rewind(data, 5, 3) == 3);
}

TEST_CASE("percent printable") {
	CHECK(LineIntelligence::percent_printable("abcd", false) == 100);
	CHECK(LineIntelligence::percent_printable("abcd okay", false) == 100);
	CHECK(LineIntelligence::percent_printable("", false) == 100);

	CHECK(LineIntelligence::percent_printable("ab\4\4", false) == 50);
	CHECK(LineIntelligence::percent_printable("ab\4\2ccdd", false) == 75);
	CHECK(LineIntelligence::percent_printable("\bcd\xff", false) == 50);
}

TEST_CASE("count occurences") {
	CHECK(LineIntelligence::count_occurrences("abcd", 'a') == 1);
	CHECK(LineIntelligence::count_occurrences("abba", 'a') == 2);
	CHECK(LineIntelligence::count_occurrences("ab....c.d", '.') == 5);
	CHECK(LineIntelligence::count_occurrences("abcd", '.') == 0);
	CHECK(LineIntelligence::count_occurrences("abcd", "a") == 1);
	CHECK(LineIntelligence::count_occurrences("abba", "a") == 2);
	CHECK(LineIntelligence::count_occurrences("abbab", "ab") == 2);
	CHECK(LineIntelligence::count_occurrences("ab....c.d", ".") == 5);
	CHECK(LineIntelligence::count_occurrences("abcd", ".") == 0);
	CHECK(LineIntelligence::count_occurrences("abcd", "abcd") == 1);
	CHECK(LineIntelligence::count_occurrences("ababababab", "abab") == 2);
	CHECK(LineIntelligence::count_occurrences("bbbaabbbabbb", "bbb") == 3);
}

TEST_CASE("heuristic timestamp") {
	string line = "the current time is 1762620592 seconds past epoch";
	optional<string> ret = LineIntelligence::apply_heuristics(line, 0);
	CHECK(ret);
	// in case timezone effect timestamp
	CHECK(ret->find(" 2025 seconds") != string::npos);
}

TEST_CASE("heuristic only timestamp") {
	string line = "1762620592";
	optional<string> ret = LineIntelligence::apply_heuristics(line, 0);
	CHECK(ret);
	// in case timezone effect timestamp
	CHECK(ret->ends_with(" 2025"));
}

TEST_CASE("heuristic only timestamp millis") {
	string line = "1762620592123";
	optional<string> ret = LineIntelligence::apply_heuristics(line, 0);
	CHECK(ret);
	// in case timezone effect timestamp
	CHECK(ret->ends_with(" 2025"));
}

TEST_CASE("base64") {
	string in = "dmVyeSBnb29kIHN0cmluZw==";
	optional<string> ret = LineIntelligence::apply_heuristics(in, 0);
	CHECK(ret);
	CHECK(*ret == "very good string");
	in[9] = '+';
	in[10] = '+';
	in[11] = '+';
	ret = LineIntelligence::apply_heuristics(in, 0);
	CHECK(!ret);
	ret = LineIntelligence::apply_heuristics(in, 1);
	CHECK(!ret);
	ret = LineIntelligence::apply_heuristics(in, 2);
	CHECK(ret);
	CHECK(ret->starts_with("very go"));
	CHECK(ret->ends_with(" string"));
}

TEST_CASE("subbase64") {
	string in = "parameter=dmVyeSBnb29kIHN0cmluZw==";
        optional<string> ret = LineIntelligence::apply_heuristics(in, 0);
        CHECK(ret);
        CHECK(*ret == "parameter=very good string");
}

TEST_CASE("hex") {
	string in = "6f6b61792074686572652068656c6c6f";
	        optional<string> ret = LineIntelligence::apply_heuristics(in, 0);
        CHECK(ret);
        CHECK(*ret == "okay there hello");
}

TEST_CASE("hex split") {
	string in = "6f6b6179 and also 7468657265 are present";
	        optional<string> ret = LineIntelligence::apply_heuristics(in, 0);
        CHECK(ret);
        CHECK(*ret == "okay and also there are present");
}

TEST_CASE("apply matches") {
	string in = "here is a string with some matches some repeat";
	map<string, string> replace;
	CHECK(LineIntelligence::apply_matches(in, replace) == nullopt);
	replace["string"] = "sentence";
	CHECK(*LineIntelligence::apply_matches(in, replace) ==
	      "here is a sentence with some matches some repeat");
	replace.clear();
	replace["some"] = "a bunch of";
	replace["here"] = "this";
	CHECK(*LineIntelligence::apply_matches(in, replace) ==
	      "this is a string with a bunch of matches a bunch of repeat");
}

TEST_CASE("newline heuristic") {
	string in = "this line has a bunch\\nof newlines at various\\npositions "
		"along the string\\nwhich can be used to indicate\\nit should "
		"break at those positions.";
	map<string, size_t> counts;
	LineIntelligence::count_occurrences(in, &counts);
	CHECK(!LineIntelligence::heuristic_json(in, counts));
	CHECK(!LineIntelligence::heuristic_code(in, counts));
	CHECK(!LineIntelligence::heuristic_httparg(in, counts));
	CHECK(LineIntelligence::heuristic_newlines(in, counts));

	auto lines = LineIntelligence::split(in, '\0');
	CHECK(lines.size() == 5);
	CHECK(lines.front()->get() == "this line has a bunch");
	CHECK(lines.back()->get() == "it should break at those positions.");
}

TEST_CASE("httparg heuristic") {
	string in = "query=string&formatted=data&x=3&y&enable&data=eyJ";
	map<string, size_t> counts;
	LineIntelligence::count_occurrences(in, &counts);
	CHECK(!LineIntelligence::heuristic_json(in, counts));
	CHECK(!LineIntelligence::heuristic_code(in, counts));
	CHECK(LineIntelligence::heuristic_httparg(in, counts));
	CHECK(!LineIntelligence::heuristic_newlines(in, counts));

	auto lines = LineIntelligence::split(in, '\0');
	CHECK(lines.size() == 6);
	CHECK(lines.front()->get() == "query=string");
	CHECK(lines.back()->get() == "data=eyJ");
}

TEST_CASE("random data") {
	mt19937 rng(random_device{}());
	uniform_int_distribution<unsigned char> chardist(33, 126);
	uniform_int_distribution<size_t> lendist(1, 1000);

	/* throw random strings at line intelligence to make sure it doesn't
	 * crash on bad input */
	for (int i = 0; i < 1000; ++i) {
		string s;
		size_t len = lendist(rng);
		while (len) {
			s += chardist(rng);
			--len;
		}
		optional<string> ret = LineIntelligence::apply_heuristics(s, 0);
	}
}

TEST_CASE("percent encoding") {
	CHECK(LineIntelligence::hex_unescape("%2F") == "/");
	CHECK(LineIntelligence::hex_unescape("%2F ") == "/ ");
	CHECK(LineIntelligence::hex_unescape(" %2F") == " /");
}

