/*

Copyright (c) 2022, Arvid Norberg
All rights reserved.

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

#include "simulator/simulator.hpp"
#include "simulator/utils.hpp"

#include "test.hpp"
#include "create_torrent.hpp"
#include "settings.hpp"
#include "setup_swarm.hpp"
#include "utils.hpp"
#include "test_utils.hpp"
#include "setup_transfer.hpp" // for addr()
#include "disk_io.hpp"

#include "libtorrent/add_torrent_params.hpp"
#include "libtorrent/alert_types.hpp"

namespace {

template <typename Setup, typename HandleAlerts>
void run_test(
	Setup setup
	, HandleAlerts on_alert
	, test_disk const downloader_disk_constructor = test_disk()
	, test_disk const seed_disk_constructor = test_disk()
	)
{
	char const* peer0_ip = "50.0.0.1";
	char const* peer1_ip = "50.0.0.2";

	lt::address peer0 = addr(peer0_ip);
	lt::address peer1 = addr(peer1_ip);

	// setup the simulation
	sim::default_config network_cfg;
	sim::simulation sim{network_cfg};
	sim::asio::io_context ios0 { sim, peer0 };
	sim::asio::io_context ios1 { sim, peer1 };

	lt::session_proxy zombie[2];

	lt::session_params params;
	// setup settings pack to use for the session (customization point)
	lt::settings_pack& pack = params.settings;
	pack = settings();

	pack.set_str(lt::settings_pack::listen_interfaces, make_ep_string(peer0_ip, false, "6881"));

	// create session
	std::shared_ptr<lt::session> ses[2];

	// session 0 is a downloader, session 1 is a seed

	params.disk_io_constructor = downloader_disk_constructor;
	ses[0] = std::make_shared<lt::session>(params, ios0);

	pack.set_str(lt::settings_pack::listen_interfaces, make_ep_string(peer1_ip, false, "6881"));

	params.disk_io_constructor = seed_disk_constructor.set_files(existing_files_mode::full_valid);
	ses[1] = std::make_shared<lt::session>(params, ios1);

	setup(*ses[0], *ses[1]);

	// only monitor alerts for session 0 (the downloader)
	print_alerts(*ses[0], [=](lt::session& ses, lt::alert const* a) {
		if (auto ta = lt::alert_cast<lt::add_torrent_alert>(a))
			ta->handle.connect_peer(lt::tcp::endpoint(peer1, 6881));
		on_alert(ses, a);
	}, 0);

	print_alerts(*ses[1], [](lt::session&, lt::alert const*){}, 1);

	// the min reconnect time defaults to 60 seconds
	sim::timer t(sim, lt::seconds(70), [&](boost::system::error_code const&)
	{
		// shut down
		int idx = 0;
		for (auto& s : ses)
		{
			zombie[idx++] = s->abort();
			s.reset();
		}
	});

	sim.run();
}

lt::info_hash_t setup_conflict(lt::session& seed, lt::session& downloader)
{
	lt::add_torrent_params atp = ::create_test_torrent(10, lt::create_flags_t{}, 2);
	atp.flags &= ~lt::torrent_flags::auto_managed;
	atp.flags &= ~lt::torrent_flags::paused;

	// add the complete torrent to the seed
	seed.async_add_torrent(atp);

	lt::info_hash_t const ih = atp.ti->info_hashes();

	// add v1-only magnet link
	atp.ti.reset();
	atp.info_hashes.v1 = ih.v1;
	atp.info_hashes.v2.clear();
	downloader.async_add_torrent(atp);

	// add v2-only magnet link
	atp.info_hashes.v1.clear();
	atp.info_hashes.v2 = ih.v2;
	downloader.async_add_torrent(atp);
	return ih;
}

} // anonymous namespace

// This adds the same hybrid torrent twice, once via the v1 info-hash and once
// via the v2 info-hash. Once the conflict is detected, both torrents should
// fail with the duplicate_torrent error state.
TORRENT_TEST(hybrid_torrent_conflict)
{
	std::vector<lt::torrent_handle> handles;

	int errors = 0;
	int conflict = 0;
	lt::info_hash_t added_ih;
	run_test([&](lt::session& ses0, lt::session& ses1) {
		added_ih = setup_conflict(ses1, ses0);
	},
	[&](lt::session& ses, lt::alert const* a) {
		if (auto const* ta = lt::alert_cast<lt::add_torrent_alert>(a))
		{
			handles.push_back(ta->handle);
		}
		else if (lt::alert_cast<lt::torrent_removed_alert>(a))
		{
			TEST_ERROR("a torrent was removed");
		}
		else if (auto const* te = lt::alert_cast<lt::torrent_error_alert>(a))
		{
			++errors;
			// both handles are expected to fail with duplicate torrent error
			TEST_EQUAL(te->error, lt::error_code(lt::errors::duplicate_torrent));
		}
		else if (auto const* tc = lt::alert_cast<lt::torrent_conflict_alert>(a))
		{
			++conflict;
			TEST_EQUAL(std::count(handles.begin(), handles.end(), tc->handle), 1);
			TEST_EQUAL(std::count(handles.begin(), handles.end(), tc->conflicting_torrent), 1);
			TEST_CHECK(tc->handle != tc->conflicting_torrent);

			TEST_CHECK(added_ih == tc->metadata->info_hashes());
		}

		for (auto& h : handles)
			TEST_CHECK(h.is_valid());

		auto torrents = ses.get_torrents();
		if (handles.size() == 2)
		{
			TEST_EQUAL(torrents.size(), 2);
		}
	}
	);

	TEST_EQUAL(errors, 2);
	TEST_EQUAL(conflict, 1);
}

// try to resume the torrents after failing with a conflict. Ensure they both
// fail again with the same error
TORRENT_TEST(resume_conflict)
{
	std::vector<lt::torrent_handle> handles;

	int errors = 0;
	int resume = 0;

	run_test([](lt::session& ses0, lt::session& ses1) {
		setup_conflict(ses1, ses0);
	},
	[&](lt::session& ses, lt::alert const* a) {
		if (auto const* ta = lt::alert_cast<lt::add_torrent_alert>(a))
		{
			handles.push_back(ta->handle);
		}
		else if (lt::alert_cast<lt::torrent_removed_alert>(a))
		{
			TEST_ERROR("a torrent was removed");
		}
		else if (auto const* te = lt::alert_cast<lt::torrent_error_alert>(a))
		{
			++errors;
			// both handles are expected to fail with duplicate torrent error
			TEST_EQUAL(te->error, lt::error_code(lt::errors::duplicate_torrent));
			if (resume < 2)
			{
				te->handle.clear_error();
				te->handle.resume();
				++resume;
			}
		}
		for (auto& h : handles)
			TEST_CHECK(h.is_valid());

		auto torrents = ses.get_torrents();
		if (handles.size() == 2)
		{
			TEST_EQUAL(torrents.size(), 2);
		}
	}
	);

	TEST_EQUAL(errors, 4);
	TEST_EQUAL(resume, 2);
}

TORRENT_TEST(resolve_conflict)
{
	int errors = 0;
	int finished = 0;
	int removed = 0;

	run_test([](lt::session& ses0, lt::session& ses1) {
		setup_conflict(ses1, ses0);
	},
	[&](lt::session& ses, lt::alert const* a) {
		if (lt::alert_cast<lt::torrent_removed_alert>(a))
		{
			++removed;
		}
		else if (auto const* te = lt::alert_cast<lt::torrent_error_alert>(a))
		{
			++errors;
			// both handles are expected to fail with duplicate torrent error
			TEST_EQUAL(te->error, lt::error_code(lt::errors::duplicate_torrent));
			if (errors == 1)
			{
				ses.remove_torrent(te->handle);
			}
			else if (errors == 2)
			{
				te->handle.clear_error();
				te->handle.resume();
			}
		}
		else if (lt::alert_cast<lt::torrent_finished_alert>(a))
		{
			++finished;
		}

		if (errors == 2)
		{
			auto torrents = ses.get_torrents();
			TEST_EQUAL(torrents.size(), 1);
		}
	});

	TEST_EQUAL(errors, 2);
	TEST_EQUAL(finished, 1);
	TEST_EQUAL(removed, 1);
}

TORRENT_TEST(conflict_readd)
{
	std::vector<lt::torrent_handle> handles;
	int errors = 0;
	int finished = 0;
	int removed = 0;
	int conflict = 0;

	run_test([](lt::session& ses0, lt::session& ses1) {
		setup_conflict(ses1, ses0);
	},
	[&](lt::session& ses, lt::alert const* a) {
		if (auto const* ta = lt::alert_cast<lt::add_torrent_alert>(a))
		{
			handles.push_back(ta->handle);
		}
		else if (lt::alert_cast<lt::torrent_removed_alert>(a))
		{
			++removed;
		}
		else if (auto const* te = lt::alert_cast<lt::torrent_error_alert>(a))
		{
			++errors;
			// both handles are expected to fail with duplicate torrent error
			TEST_EQUAL(te->error, lt::error_code(lt::errors::duplicate_torrent));
		}
		else if (auto const* tf = lt::alert_cast<lt::torrent_finished_alert>(a))
		{
			++finished;
			TEST_EQUAL(handles.size(), 1);
			TEST_CHECK(handles[0] == tf->handle);
		}
		else if (auto const* tc = lt::alert_cast<lt::torrent_conflict_alert>(a))
		{
			++conflict;
			ses.remove_torrent(tc->handle);
			ses.remove_torrent(tc->conflicting_torrent);
			handles.clear();

			lt::add_torrent_params atp;
			atp.ti = std::move(tc->metadata);
			atp.save_path = ".";
			ses.async_add_torrent(std::move(atp));
		}

		for (auto& h : handles)
			TEST_CHECK(h.is_valid());
	});

	TEST_EQUAL(errors, 2);
	TEST_EQUAL(finished, 1);
	TEST_EQUAL(removed, 2);
	TEST_EQUAL(conflict, 1);
}
