/*
 * Logserver
 * Copyright (C) 2017-2025 Joel Reardon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "test.h"

#include <random>

#include "ncurses.h"
#include "mock_log_lines_for_filter.h"
#include "line_filter_keyword.h"

using namespace std;

TEST_CASE("line filter random") {
	mt19937 rng(random_device{}());
        uniform_int_distribution<size_t> length_dist(1, 25);
        uniform_int_distribution<size_t> percent_dist(0, 100);
        uniform_int_distribution<> invert_dist(0, 1);
        uniform_int_distribution<int> anchor_dist(0, 2);
	for (int j = 0; j < 10; ++j) {
		set<size_t> all;
		set<size_t> matches;
		size_t len = length_dist(rng);
		int anchor = anchor_dist(rng);
		int parms = Match::PRESENT | Match::ANCHOR_NONE;
		bool not_inverted = invert_dist(rng);
		if (!not_inverted) parms |= Match::MISSING;
		if (anchor == 1) parms |= Match::ANCHOR_LEFT;
		if (anchor == 2) parms |= Match::ANCHOR_RIGHT;
		len *= len;
		len *= len;
		string keyword = "some match";

		size_t percent = percent_dist(rng);
		MockLogLinesForFilter ll(len, parms, keyword);
		for (size_t i = 0; i < len; ++i) {
			all.insert(i);
			if (percent_dist(rng) < percent) {
				ll.expect_match(i);
				if (not_inverted) matches.insert(i);
			} else if (!not_inverted) matches.insert(i);
		}

		LineFilterKeyword lfk(ll, parms, 0);
		lfk.current_type(keyword);
		lfk.finish();

		CHECK(lfk.get_keyword() == keyword);
		CHECK(!lfk.empty());

		while (!ll.checked_all()) usleep(1000);

		for (size_t i = 0; i < len; ++i) {
			if (matches.count(i)) CHECK(lfk.is_match(i));
			else CHECK(!lfk.is_match(i));
		}

		set<size_t> disjunction;
		// empty set gets all matches
		lfk.disjunctive_join(&disjunction);
		CHECK(disjunction == matches);
		// full set becomes all matches
		lfk.conjunctive_join(&all);
		CHECK(all == matches);
		// all matches stays the same
		lfk.conjunctive_join(&all);
		CHECK(all == matches);
	}
}

void check_num_matches_eq(const LineFilterKeyword& lfk,
		          size_t num) {
	set<size_t> disjunction;
	lfk.disjunctive_join(&disjunction);
	CHECK(disjunction.size() == num);
}

void check_num_matches_le(const LineFilterKeyword& lfk,
		          size_t num) {
	set<size_t> disjunction;
	lfk.disjunctive_join(&disjunction);
	CHECK(disjunction.size() <= num);
}

void sleep_until_checked(const MockLogLinesForFilter& ll,
			 size_t matched,
			 const LineFilterKeyword& lfk,
                         size_t num) {
	while (!ll.checked_number(matched)) {
		check_num_matches_le(lfk, num);
		usleep(100);
	}
}

TEST_CASE("line filter keystroke following") {
	mt19937 rng(random_device{}());
	MockLogLinesForFilter ll(
		5000, 0);
	ll.expect("va", 10);
	ll.expect("a", 10);
	ll.expect("ab", 10);
	ll.expect("abc", 10);
	ll.expect("abcd", 10);

	LineFilterKeyword lfk(ll, Match::PRESENT, 0);
	lfk.current_type("");
	usleep(1000);
	CHECK(ll.checked_number(0));
	lfk.current_type("a");
	sleep_until_checked(ll, 5000, lfk, 50);
	check_num_matches_eq(lfk, 50);

	lfk.current_type("va");
	sleep_until_checked(ll, 10000, lfk, 10);
	check_num_matches_eq(lfk, 10);

	lfk.current_type("a");
	sleep_until_checked(ll, 15000, lfk, 50);
	check_num_matches_eq(lfk, 50);

	lfk.current_type("ab");
	sleep_until_checked(ll, 20000, lfk, 30);
	check_num_matches_eq(lfk, 30);

	lfk.current_type("abc");
	sleep_until_checked(ll, 25000, lfk, 20);
	check_num_matches_eq(lfk, 20);

	lfk.current_type("abcd");
	sleep_until_checked(ll, 30000, lfk, 10);
	while (!ll.checked_number(30000)) usleep(1000);
	check_num_matches_eq(lfk, 10);
	lfk.finish();
	check_num_matches_eq(lfk, 10);
	// checks that after finishing the last of .. means complete
	CHECK(lfk.get_description() == "abcd");
}

/* Starts a search from the end position, checks that it always grows upwards
 * and has .. while the search is ongoing and results are available. */
TEST_CASE("line filter append") {
	mt19937 rng(random_device{}());
	MockLogLinesForFilter ll(
		100000, 0);
	ll.expect("abcd", 1000);

	LineFilterKeyword lfk(ll, Match::PRESENT, 0);
	lfk.current_type("abcd");
	lfk.finish();
	lfk.set_whence(G::NO_POS);
	lfk.post_end(110000);
	lfk.post_end(120000);
	lfk.post_end(130000);
	lfk.post_end(140000);
	size_t earliest = G::NO_POS;
	size_t last_size = 4;
	while (true) {
		string name = lfk.get_description();
		set<size_t> vals;
		lfk.disjunctive_join(&vals);
		CHECK(vals.size() >= last_size);
		CHECK(*vals.begin() <= earliest);
		if (*vals.begin() == earliest) {
			CHECK(vals.size() == last_size);
		}
		// we are done searching
		if (vals.size() == 1004) {
			CHECK(name == "abcd");
			break;
		}
		// we are still seaching
		CHECK(name == "abcd..");
		last_size = vals.size();
		earliest = *vals.begin();
		CHECK(lfk.next_match(earliest + 1, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 10, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 100, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 1000, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 10000, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 100000, G::DIR_UP) != G::NO_POS);
		CHECK(lfk.next_match(earliest + 10, G::DIR_DOWN) != G::NO_POS);
		CHECK(lfk.next_match(141000, G::DIR_UP) == 140000);
		CHECK(lfk.next_match(141000, G::DIR_DOWN) == G::NO_POS);
		usleep(100);
	}
	CHECK(ll.checked_number(100000));
}

TEST_CASE("line filter string cast") {
	MockLogLinesForFilter ll(
                5000, 0);
	LineFilterKeyword lfk(ll, Match::PRESENT, 0);
	CHECK(static_cast<string>(lfk) == "");
	lfk.current_type("abcd");
	CHECK(static_cast<string>(lfk) == "abcd");
	lfk.finish();
	CHECK(static_cast<string>(lfk) == "abcd");
}

