/*

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);
}

namespace {

	void check_status_durations(lt::torrent_handle const& handle, lt::seconds expected)
	{
		torrent_status const st = handle.status();
		TEST_EQUAL(st.active_duration.count(), expected.count());
		TEST_EQUAL(st.seeding_duration.count(), expected.count());
		TEST_EQUAL(st.finished_duration.count(), expected.count());
	}

	// removing a torrent must not let active_time(), seeding_time() or
	// finished_time() include time it shouldn't: not time spent paused
	// (pause_before_remove == true), and not lose live time that was never
	// paused (pause_before_remove == false). Both cases exercise
	// torrent::abort(), which clears m_paused directly, without going through
	// do_resume(), so these timers rely on m_abort (in addition to
	// is_paused()) to know their became-active/seed/finished timestamps are
	// stale -- and abort() itself must flush the live segment into the
	// accumulators before that happens, the same way do_pause() does.
	void run_status_timers_removed_test(bool const pause_before_remove)
	{
		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;
				}
			}
			// terminate
			,
			[&](int ticks, lt::session& ses) -> 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 10:
						if (pause_before_remove)
						{
							// pause. this flushes and freezes active/seeding/finished
							// time at expected_active_duration
							handle.pause();
							tick_is_in_active_range = false;
						}
						break;
					case 5000:
						// removing the torrent -- whether it's been paused for a
						// long time (that gap must not leak into the timers) or
						// has been actively seeding the whole time (that time
						// must not be lost)
						ses.remove_torrent(handle);
						check_status_durations(handle, expected_active_duration);
						ran_to_completion = true;
						return true;
				}

				// while paused, the durations must stay exactly frozen, tick by
				// tick, for the whole stretch leading up to the removal above
				if (pause_before_remove && !tick_is_in_active_range && ticks > 0)
				{
					check_status_durations(handle, expected_active_duration);
				}
				return false;
			});
		TEST_CHECK(ran_to_completion);
	}

} // namespace

TORRENT_TEST(status_timers_removed_while_paused) { run_status_timers_removed_test(true); }

TORRENT_TEST(status_timers_removed_while_active) { run_status_timers_removed_test(false); }

// force-rechecking a torrent that's actively seeding must not discard the
// seeding time accrued before the recheck, nor inflate it. force_recheck()
// clears m_have_all directly, which is_seed() depends on, so the live time
// since m_became_seed must be flushed into m_seeding_time before that
// happens, and m_became_seed must be re-anchored to the moment the recheck
// completes and confirms the torrent is a seed again.
TORRENT_TEST(seeding_time_survives_force_recheck)
{
	bool ran_to_completion = false;
	lt::torrent_handle handle;
	seconds32 seeding_time_before_recheck{0};
	int recheck_started_tick = -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))
			{
				handle = ta->handle;
			}
		}
		// terminate
		,
		[&](int ticks, lt::session&) -> bool {
			if (ticks == 5000)
			{
				// let a good amount of real seeding time accrue first, then
				// force a recheck of the (still intact) data
				seeding_time_before_recheck = seconds32(handle.status().seeding_duration);
				handle.force_recheck();
				recheck_started_tick = ticks;
			}

			if (recheck_started_tick != -1)
			{
				torrent_status const st = handle.status();
				if (st.state == torrent_status::seeding)
				{
					// the recheck completed and confirmed we're still a
					// complete seed. seeding_duration must equal the
					// pre-recheck total plus only the (small) time since
					// the recheck itself completed
					seconds32 const elapsed_since_recheck_started(ticks - recheck_started_tick);
					TEST_CHECK(seconds32(st.seeding_duration) >= seeding_time_before_recheck);
					TEST_CHECK(seconds32(st.seeding_duration)
						<= seeding_time_before_recheck + elapsed_since_recheck_started);
					ran_to_completion = true;
					return true;
				}
				if (ticks > recheck_started_tick + 2000)
				{
					TEST_ERROR("recheck did not complete in time");
					return true;
				}
			}
			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();
	});
}
