/*

Copyright (c) 2018, Steven Siloti
Copyright (c) 2015-2021, Arvid Norberg
Copyright (c) 2016, 2018, 2020, Alden Torres
Copyright (c) 2018, d-komarov
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include "test.hpp"
#include "test_utils.hpp"
#include "libtorrent/aux_/alert_manager.hpp"
#include "libtorrent/torrent_handle.hpp"
#include "libtorrent/alert_types.hpp"
#include "libtorrent/extensions.hpp"
#include "setup_transfer.hpp"

#include <functional>
#include <thread>

using namespace lt;

TORRENT_TEST(limit)
{
	aux::alert_manager mgr(500, alert_category::all);

	TEST_EQUAL(mgr.alert_queue_size_limit(), 500);
	TEST_EQUAL(mgr.pending(), false);

	// try add 600 torrent_add_alert to make sure we honor the limit of 500
	// alerts.
	for (auto i = 0_piece; i < 600_piece; ++i)
		mgr.emplace_alert<piece_finished_alert>(torrent_handle(), i);

	TEST_EQUAL(mgr.pending(), true);

	std::vector<alert*> alerts;
	mgr.get_all(alerts);

	// even though we posted 600, the limit was 500
	// +1 for the alerts_dropped_alert
	TEST_EQUAL(alerts.size(), 501);

	TEST_EQUAL(mgr.pending(), false);

	// now, try lowering the limit and do the same thing again
	mgr.set_alert_queue_size_limit(200);

	for (auto i = 0_piece; i < 600_piece; ++i)
		mgr.emplace_alert<piece_finished_alert>(torrent_handle(), i);

	TEST_EQUAL(mgr.pending(), true);

	mgr.get_all(alerts);

	// even though we posted 600, the limit was 200
	// +1 for the alerts_dropped_alert
	TEST_EQUAL(alerts.size(), 201);
}

TORRENT_TEST(limit_int_max)
{
	int const inf = std::numeric_limits<int>::max();
	aux::alert_manager mgr(inf, alert_category::all);

	TEST_EQUAL(mgr.alert_queue_size_limit(), inf);

	for (auto i = 0_piece; i < 600_piece; ++i)
		mgr.emplace_alert<piece_finished_alert>(torrent_handle(), i);

	for (auto i = 0_piece; i < 600_piece; ++i)
		mgr.emplace_alert<torrent_removed_alert>(torrent_handle(), info_hash_t(), client_data_t{});

	std::vector<alert*> alerts;
	mgr.get_all(alerts);

	TEST_EQUAL(alerts.size(), 1200);
}

TORRENT_TEST(priority_limit)
{
	aux::alert_manager mgr(100, alert_category::all);

	TEST_EQUAL(mgr.alert_queue_size_limit(), 100);

	// this should only add 100 because of the limit
	for (auto i = 0_piece; i < 200_piece; ++i)
		mgr.emplace_alert<piece_finished_alert>(torrent_handle(), i);

	// the limit is twice as high for priority alerts
	for (auto i = 0_file; i < 300_file; ++i)
		mgr.emplace_alert<file_rename_failed_alert>(torrent_handle(), i, error_code());

	std::vector<alert*> alerts;
	mgr.get_all(alerts);

	// even though we posted 500, the limit was 100 for half of them and
	// 100 + 200 for the other half, meaning we should have 300 alerts now
	// +1 for the alerts_dropped_alert
	TEST_EQUAL(alerts.size(), 301);
}

namespace {
void test_notify_fun(int& cnt)
{
	++cnt;
}
} // anonymous namespace

TORRENT_TEST(notify_function)
{
	int cnt = 0;
	aux::alert_manager mgr(100, alert_category::all);

	TEST_EQUAL(mgr.alert_queue_size_limit(), 100);
	TEST_EQUAL(mgr.pending(), false);

	for (int i = 0; i < 20; ++i)
		mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	TEST_EQUAL(mgr.pending(), true);

	// if there are queued alerts when we set the notify function,
	// that counts as an edge and it's called
	mgr.set_notify_function(std::bind(&test_notify_fun, std::ref(cnt)));

	TEST_EQUAL(mgr.pending(), true);
	TEST_EQUAL(cnt, 1);

	// subsequent posted alerts will not cause an edge (because there are
	// already alerts queued)
	for (int i = 0; i < 20; ++i)
		mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	TEST_EQUAL(mgr.pending(), true);
	TEST_EQUAL(cnt, 1);

	// however, if we pop all the alerts and post new ones, there will be
	// and edge triggering the notify call
	std::vector<alert*> alerts;
	mgr.get_all(alerts);

	TEST_EQUAL(mgr.pending(), false);

	for (int i = 0; i < 20; ++i)
		mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	TEST_EQUAL(mgr.pending(), true);
	TEST_EQUAL(cnt, 2);
}

#ifndef TORRENT_DISABLE_EXTENSIONS
namespace {
int plugin_alerts[3] = { 0, 0, 0 };

struct test_plugin : lt::plugin
{
	explicit test_plugin(int index) : m_index(index) {}
	void on_alert(alert const*) override
	{
		++plugin_alerts[m_index];
	}
	int m_index;
};
} // anonymous namespace
#endif

TORRENT_TEST(extensions)
{
#ifndef TORRENT_DISABLE_EXTENSIONS
	memset(plugin_alerts, 0, sizeof(plugin_alerts));
	aux::alert_manager mgr(100, alert_category::all);

	mgr.add_extension(std::make_shared<test_plugin>(0));
	mgr.add_extension(std::make_shared<test_plugin>(1));
	mgr.add_extension(std::make_shared<test_plugin>(2));

	for (int i = 0; i < 53; ++i)
		mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	TEST_EQUAL(plugin_alerts[0], 53);
	TEST_EQUAL(plugin_alerts[1], 53);
	TEST_EQUAL(plugin_alerts[2], 53);

	for (int i = 0; i < 17; ++i)
		mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	TEST_EQUAL(plugin_alerts[0], 70);
	TEST_EQUAL(plugin_alerts[1], 70);
	TEST_EQUAL(plugin_alerts[2], 70);
#endif
}

/*
namespace {

void post_torrent_added(aux::alert_manager* mgr)
{
	std::this_thread::sleep_for(lt::milliseconds(10));
	mgr->emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());
}

} // anonymous namespace

// this test is too flaky

TORRENT_TEST(wait_for_alert)
{
	aux::alert_manager mgr(100, alert_category::all);

	time_point start = clock_type::now();

	alert* a = mgr.wait_for_alert(seconds(1));

	time_point end = clock_type::now();
	TEST_EQUAL(a, static_cast<alert*>(nullptr));
	std::printf("delay: %d ms (expected 1 second)\n"
		, int(total_milliseconds(end - start)));
	TEST_CHECK(end - start > milliseconds(900));
	TEST_CHECK(end - start < milliseconds(1100));

	mgr.emplace_alert<add_torrent_alert>(torrent_handle(), add_torrent_params(), error_code());

	start = clock_type::now();
	a = mgr.wait_for_alert(seconds(1));
	end = clock_type::now();

	std::printf("delay: %d ms\n", int(total_milliseconds(end - start)));
	TEST_CHECK(end - start < milliseconds(1));
	TEST_CHECK(a->type() == add_torrent_alert::alert_type);

	std::vector<alert*> alerts;
	mgr.get_all(alerts);

	start = clock_type::now();
	std::thread posting_thread(&post_torrent_added, &mgr);

	a = mgr.wait_for_alert(seconds(10));
	end = clock_type::now();

	std::printf("delay: %d ms\n", int(total_milliseconds(end - start)));
	TEST_CHECK(end - start < milliseconds(500));
	TEST_CHECK(a->type() == add_torrent_alert::alert_type);

	posting_thread.join();
}
*/

TORRENT_TEST(alert_mask)
{
	aux::alert_manager mgr(100, alert_category::all);

	TEST_CHECK(mgr.should_post<add_torrent_alert>());
	TEST_CHECK(mgr.should_post<torrent_paused_alert>());

	mgr.set_alert_mask({});

	TEST_CHECK(!mgr.should_post<add_torrent_alert>());
	TEST_CHECK(!mgr.should_post<torrent_paused_alert>());
}

TORRENT_TEST(get_all_empty)
{
	aux::alert_manager mgr(100, alert_category::all);
	std::vector<alert*> alerts(10);

	mgr.get_all(alerts);

	TEST_CHECK(alerts.empty());
}

TORRENT_TEST(dropped_alerts)
{
	aux::alert_manager mgr(1, alert_category::all);

	// nothing has dropped yet
	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	// still nothing, there's space for one alert
	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	// still nothing, there's space for one alert
	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	// that last alert got dropped though, since it would have brought the queue
	// size to 3
	std::vector<alert*> alerts;
	mgr.get_all(alerts);
	auto const d = alert_cast<alerts_dropped_alert>(alerts.back())->dropped_alerts;
	TEST_EQUAL(d.count(), 1);
	TEST_CHECK(d.test(torrent_finished_alert::alert_type));
}

TORRENT_TEST(alerts_dropped_alert)
{
	aux::alert_manager mgr(1, alert_category::all);

	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	mgr.emplace_alert<torrent_finished_alert>(torrent_handle());
	// that last alert got dropped though, since it would have brought the queue
	// size to 3
	std::vector<alert*> alerts;
	mgr.get_all(alerts);

#ifndef TORRENT_DISABLE_ALERT_MSG
	TEST_EQUAL(alerts.back()->message(), "dropped alerts: torrent_finished ");
#endif
	auto* a = lt::alert_cast<alerts_dropped_alert>(alerts.back());
	TEST_CHECK(a);
	TEST_CHECK(a->dropped_alerts[torrent_finished_alert::alert_type] == true);
}

#ifndef TORRENT_DISABLE_EXTENSIONS
struct post_plugin : lt::plugin
{
	explicit post_plugin(aux::alert_manager& m) : mgr(m) {}
	void on_alert(alert const*) override
	{
		if (++depth > 10) return;
		mgr.emplace_alert<piece_finished_alert>(torrent_handle(), 0_piece);
	}

	aux::alert_manager& mgr;
	int depth = 0;
};

// make sure the alert manager supports alerts being posted while executing a
// plugin handler
TORRENT_TEST(recursive_alerts)
{
	aux::alert_manager mgr(100, alert_category::all);
	auto pl = std::make_shared<post_plugin>(mgr);
	mgr.add_extension(pl);

	mgr.emplace_alert<piece_finished_alert>(torrent_handle(), 0_piece);

	TEST_EQUAL(pl->depth, 11);
}

#endif // TORRENT_DISABLE_EXTENSIONS
