/*

Copyright (c) 2015-2018, 2020-2022, Arvid Norberg
Copyright (c) 2017, Falcosc
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 "setup_swarm.hpp"
#include "utils.hpp"
#include "simulator/simulator.hpp"
#include "libtorrent/alert_types.hpp"
#include "libtorrent/settings_pack.hpp"
#include "libtorrent/session.hpp"

#include <iostream>

using namespace lt;
using namespace sim;

time_point32 time_now()
{
	return lt::time_point_cast<seconds32>(lt::clock_type::now());
}

template <typename Tp1, typename Tp2>
bool eq(Tp1 const lhs, Tp2 const rhs)
{
	return std::abs(lt::duration_cast<seconds>(lhs - rhs).count()) <= 2;
}

// this is a test for torrent_status time counters are correct
TORRENT_TEST(status_timers)
{
	lt::time_point32 start_time;
	lt::torrent_handle handle;
	bool ran_to_completion = false;

	setup_swarm(1, swarm_test::upload
		// add session
		, [](lt::settings_pack&) {}
		// add torrent
		, [](lt::add_torrent_params&) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				start_time = time_now();
				handle = ta->handle;
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{

			// simulate 20 hours of uptime. Currently, the session_time and internal
			// peer timestamps are 16 bits counting seconds, so they can only
			// represent about 18 hours. The clock steps forward in 4 hour increments
			// to stay within that range
			if (ticks > 20 * 60 * 60)
			{
				ran_to_completion = true;
				return true;
			}

			// once an hour, verify that the timers seem correct
			if ((ticks % 3600) == 0)
			{
				lt::time_point32 const now = time_now();
				// finish is 1 tick after start
				auto const since_finish = duration_cast<seconds>(now - start_time);
				torrent_status st = handle.status();
				TEST_EQUAL(st.active_duration.count(), since_finish.count());
				TEST_EQUAL(st.seeding_duration.count(), since_finish.count());
				TEST_EQUAL(st.finished_duration.count(), since_finish.count());

				// does not upload without peers
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				// does not download in seeding mode
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
			return false;
		});
	TEST_CHECK(ran_to_completion);
}

TORRENT_TEST(status_timers_last_upload)
{
	bool ran_to_completion = false;

	lt::torrent_handle handle;

	setup_swarm(2, swarm_test::upload
		// add session
		, [](lt::settings_pack&) {}
		// add torrent
		, [](lt::add_torrent_params&) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				handle = ta->handle;
				torrent_status st = handle.status();
				// test last upload and download state before we go through
				// torrent states
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{
			if (ticks > 10)
			{
				ran_to_completion = true;
				return true;
			}

			torrent_status st = handle.status();
			// upload time is 0 seconds behind now
			TEST_CHECK(eq(st.last_upload, time_now()));
			// does not download in seeding mode
			TEST_CHECK(st.last_download == time_point(seconds(0)));
			return false;
		});
	TEST_CHECK(ran_to_completion);
}

TORRENT_TEST(status_timers_time_shift_with_active_torrent)
{
	bool ran_to_completion = false;

	lt::torrent_handle handle;
	seconds expected_active_duration = seconds(1);
	bool tick_is_in_active_range = false;
	int tick_check_intervall = 1;

	setup_swarm(1, swarm_test::upload
		// add session
		, [](lt::settings_pack&) {}
		// add torrent
		, [](lt::add_torrent_params&) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				handle = ta->handle;
				torrent_status st = handle.status();
				// test last upload and download state before we go through
				// torrent states
				TEST_CHECK(st.last_download == time_point(seconds(0)));
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{
			if(tick_is_in_active_range){
				// 1 second per tick
				expected_active_duration++;
			}

			switch(ticks)
			{
				case 0:
					// torrent get ready for seeding on first tick, means time +1s
					tick_is_in_active_range = true;
					break;
				case 1:
					// pause after we did have the first upload tick
					handle.pause();
					tick_is_in_active_range = false;
					break;
				case 64000:
					// resume just before we hit the time shift handling
					// this is needed to test what happens if we want to
					// shift more time then we have active time because
					// we shift 4 hours and have less then 1 hour active time
					handle.resume();
					tick_is_in_active_range = true;
					// don't check every tick
					tick_check_intervall = 600;
					break;
				case 68000:
					// simulate at least 68000 seconds because timestamps are
					// 16 bits counting seconds
					ran_to_completion = true;
					return true;
			}

			// verify that the timers seem correct
			if (tick_is_in_active_range && (ticks % tick_check_intervall) == 0)
			{
				torrent_status st = handle.status();
				TEST_EQUAL(st.active_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.seeding_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.finished_duration.count(), expected_active_duration.count());
				// does not upload without peers
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				// does not download in seeding mode
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
			return false;
		});
	TEST_CHECK(ran_to_completion);
}

TORRENT_TEST(finish_time_shift_active)
{
	bool ran_to_completion = false;

	lt::torrent_handle handle;
	seconds expected_active_duration = seconds(1);
	bool tick_is_in_active_range = false;

	setup_swarm(1, swarm_test::upload
		// add session
		, [](lt::settings_pack&) {}
		// add torrent
		, [](lt::add_torrent_params&) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				handle = ta->handle;
				torrent_status st = handle.status();
				// test last upload and download state before we go through
				// torrent states
				TEST_CHECK(st.last_download == time_point(seconds(0)));
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{
			if(tick_is_in_active_range){
				// 1 second per tick
				expected_active_duration++;
			}

			switch(ticks)
			{
				case 0:
					// torrent get ready for seeding on first tick, means time +1s
					tick_is_in_active_range = true;
					break;
				case 7000:
					// pause before 4 hours to get a become finish timestamp which
					// will be clamped
					handle.pause();
					// resume to get an become finish update
					handle.resume();
					tick_is_in_active_range = true;
					break;
				case 70000:
					// simulate at least 70000 seconds because timestamps are
					// 16 bits counting seconds
					ran_to_completion = true;
					return true;
			}

			// verify that the timers seem correct
			if ((ticks % 3600) == 0)
			{
				torrent_status st = handle.status();
				TEST_EQUAL(st.active_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.seeding_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.finished_duration.count(), expected_active_duration.count());
				// does not upload without peers
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				// does not download in seeding mode
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
			return false;
		});
	TEST_CHECK(ran_to_completion);
}

TORRENT_TEST(finish_time_shift_paused)
{
	bool ran_to_completion = false;

	lt::torrent_handle handle;
	seconds expected_active_duration = seconds(1);
	bool tick_is_in_active_range = false;

	setup_swarm(1, swarm_test::upload
		// add session
		, [](lt::settings_pack&) {}
		// add torrent
		, [](lt::add_torrent_params&) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				handle = ta->handle;
				torrent_status st = handle.status();
				// test last upload and download state before we go through
				// torrent states
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{
			if(tick_is_in_active_range){
				// 1 second per tick
				expected_active_duration++;
			}

			switch(ticks)
			{
				case 0:
					// torrent get ready for seeding on first tick, means time +1s
					tick_is_in_active_range = true;
					break;
				case 7000:
					// pause before 4 hours to get a become finish timestamp which
					// will be clamped
					handle.pause();
					// resume to get an become finish update
					handle.resume();
					// pause to test timeshift in paused state
					handle.pause();
					tick_is_in_active_range = false;
					break;
				case 70000:
					// simulate at least 70000 seconds because timestamps are
					// 16 bits counting seconds
					ran_to_completion = true;
					return true;
			}

			// verify that the timers seem correct
			if (tick_is_in_active_range && (ticks % 3600) == 0)
			{
				torrent_status st = handle.status();
				TEST_EQUAL(st.active_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.seeding_duration.count(), expected_active_duration.count());
				TEST_EQUAL(st.finished_duration.count(), expected_active_duration.count());
				// does not upload without peers
				TEST_CHECK(st.last_upload == time_point(seconds(0)));
				// does not download in seeding mode
				TEST_CHECK(st.last_download == time_point(seconds(0)));
			}
			return false;
		});
	TEST_CHECK(ran_to_completion);
}

// This test makes sure that adding a torrent causes no torrent related alert to
// be posted _before_ the add_torrent_alert, which is expected to always be the
// first
TORRENT_TEST(alert_order)
{
#if TORRENT_ABI_VERSION == 1
	bool received_torrent_add_alert = false;
#endif
	bool received_add_torrent_alert = false;
	int num_torrent_alerts = 0;

	lt::torrent_handle handle;

	setup_swarm(1, swarm_test::upload
		// add session
		, [](lt::settings_pack& sett) {
			sett.set_int(settings_pack::alert_mask, alert_category::all);
		}
		// add torrent
		, [](lt::add_torrent_params ) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
#if TORRENT_ABI_VERSION == 1
			if (alert_cast<torrent_added_alert>(a))
			{
				TEST_EQUAL(received_torrent_add_alert, false);
				received_torrent_add_alert = true;
			}
			else
#endif
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
#if TORRENT_ABI_VERSION == 1
				TEST_EQUAL(received_torrent_add_alert, true);
#endif
				TEST_EQUAL(received_add_torrent_alert, false);
				received_add_torrent_alert = true;
				handle = ta->handle;
			}
			else if (dynamic_cast<torrent_log_alert const*>(a))
			{
				// it's acceptable to receive torrent_log_alert before
				// add_torrent_alert, since they are for debugging
				return;
			}
			else if (auto ta = dynamic_cast<torrent_alert const*>(a))
			{
				TEST_EQUAL(received_add_torrent_alert, true);
				TEST_CHECK(handle == ta->handle);
				++num_torrent_alerts;
			}
		}
		// terminate
		, [&](int ticks, lt::session&) -> bool
		{ return ticks > 10; }
	);

	TEST_EQUAL(received_add_torrent_alert, true);
	TEST_CHECK(num_torrent_alerts > 1);
}

// this tests a torrent completing the download when `active_seeds` is set to 0.
// the torrent should be paused when it completes the download
TORRENT_TEST(active_timer_no_seed)
{
	lt::time_point32 start_time;
	lt::torrent_handle handle;
	bool ran_to_completion = false;

	int active_time = 0;

	setup_swarm(4, swarm_test::download
		// add session
		, [](lt::settings_pack& p ) {
			p.set_int(settings_pack::active_seeds, 0);
			p.set_bool(settings_pack::dont_count_slow_torrents, false);
		}
		// add torrent
		, [](lt::add_torrent_params& p) {
			p.flags |= torrent_flags::auto_managed;
		}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				start_time = time_now();
				handle = ta->handle;
			}
		}
		// terminate
		, [&](int const ticks, lt::session& ses) -> bool
		{
			if (!is_seed(ses))
			{
				++active_time;
			}
			else
			{
				// some part of the simulation is not deterministic, and causes this to vary
				// between platforms/compilers
				TEST_CHECK(active_time >= 6);
				TEST_CHECK(active_time <= 9);
			}

			torrent_status st = handle.status();
			TEST_EQUAL(st.active_duration.count(), active_time);
			TEST_EQUAL(st.seeding_duration.count(), 0);
			TEST_EQUAL(st.finished_duration.count(), 0);

			// does not upload without peers
			TEST_CHECK(st.last_upload == time_point(seconds(0)));

			if (ticks > 2 * 60)
			{
				ran_to_completion = true;
				return true;
			}

			return false;
		});
	TEST_CHECK(ran_to_completion);
}

template <typename PauseFun>
void test_pause(PauseFun f)
{
	lt::time_point32 start_time;
	lt::torrent_handle handle;
	bool ran_to_completion = false;

	int const pause_time = 5;

	int active_time = 0;

	int paused_alert_count = 0;

	setup_swarm(5, swarm_test::download
		// add session
		, [](lt::settings_pack& p ) {}
		// add torrent
		, [](lt::add_torrent_params& p) {}
		// on alert
		, [&](lt::alert const* a, lt::session&) {
			if (auto ta = alert_cast<add_torrent_alert>(a))
			{
				TEST_CHECK(!handle.is_valid());
				start_time = time_now();
				handle = ta->handle;
			}
			if (alert_cast<torrent_paused_alert>(a))
			{
				++paused_alert_count;
			}
		}
		// terminate
		, [&](int const ticks, lt::session& ses) -> bool
		{
			if (ticks == pause_time)
			{
				f(handle, ses);
			}
			if (ticks <= pause_time)
				++active_time;

			torrent_status st = handle.status();
			TEST_EQUAL(st.active_duration.count(), active_time);
			TEST_EQUAL(st.seeding_duration.count(), 0);
			TEST_EQUAL(st.finished_duration.count(), 0);

			// does not upload without peers
			TEST_CHECK(st.last_upload == time_point(seconds(0)));

			if (ticks > 2 * 60)
			{
				ran_to_completion = true;
				return true;
			}

			return false;
		});
	TEST_EQUAL(paused_alert_count, 1);
	TEST_CHECK(ran_to_completion);
}

TORRENT_TEST(active_timer_graceful_pause)
{
	test_pause([](lt::torrent_handle h, lt::session&)
	{
		h.pause(torrent_handle::graceful_pause);
	});
}

TORRENT_TEST(active_timer_pause)
{
	test_pause([](lt::torrent_handle h, lt::session&)
	{
		h.pause();
	});
}

TORRENT_TEST(active_timer_session_pause)
{
	test_pause([](lt::torrent_handle, lt::session& s)
	{
		s.pause();
	});
}
