/*

Copyright (c) 2014-2018, 2020-2021, Arvid Norberg
Copyright (c) 2016, 2018, 2020-2021, Alden Torres
All rights reserved.

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

#include "libtorrent/aux_/peer_list.hpp"
#include "libtorrent/torrent_handle.hpp"
#include "libtorrent/aux_/torrent_peer_allocator.hpp"
#include "libtorrent/peer_connection_interface.hpp"
#include "libtorrent/aux_/stat.hpp"
#include "libtorrent/aux_/ip_voter.hpp"
#include "libtorrent/ip_filter.hpp"
#include "libtorrent/peer_info.hpp"
#include "libtorrent/aux_/random.hpp"
#include "libtorrent/aux_/socket_io.hpp"

#include "test.hpp"
#include "setup_transfer.hpp"
#include <vector>
#include <memory> // for shared_ptr
#include <cstdarg>

using namespace lt;
using namespace lt::aux;

namespace {

struct mock_torrent;

struct mock_peer_connection
	: peer_connection_interface
	, std::enable_shared_from_this<mock_peer_connection>
{
	mock_peer_connection(mock_torrent* tor, bool out)
		: m_choked(false)
		, m_outgoing(out)
		, m_tp(nullptr)
		, m_our_id(nullptr)
		, m_disconnect_called(false)
		, m_torrent(*tor)
	{
		aux::random_bytes(m_id);
	}

	mock_peer_connection(mock_torrent* tor, bool out, tcp::endpoint const& remote)
		: mock_peer_connection(tor, out)
	{
		m_remote = remote;
		m_local = ep("127.0.0.1", 8080);
	}

#if TORRENT_USE_I2P
	mock_peer_connection(mock_torrent* tor, bool out, std::string remote)
		: mock_peer_connection(tor, out)
	{
		m_i2p_destination = std::move(remote);
	}
#endif

	virtual ~mock_peer_connection() = default;

#if !defined TORRENT_DISABLE_LOGGING
	bool should_log(peer_log_alert::direction_t) const noexcept override
	{ return true; }

	void peer_log(peer_log_alert::direction_t, peer_log_alert::event_t
		, char const* fmt, ...) const noexcept override
	{
		va_list v;
		va_start(v, fmt);
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-nonliteral"
#endif
		std::vprintf(fmt, v);
#ifdef __clang__
#pragma clang diagnostic pop
#endif
		va_end(v);
	}
#endif

	bool was_disconnected() const { return m_disconnect_called; }
	void set_local_ep(tcp::endpoint const& ep) { m_local = ep; }

	aux::stat m_stat;
	bool m_choked;
	bool m_outgoing;
	torrent_peer* m_tp;
	tcp::endpoint m_remote;
	tcp::endpoint m_local;
#if TORRENT_USE_I2P
	std::string m_i2p_destination;
	std::string m_local_i2p_endpoint;
#endif
	peer_id m_id;
	peer_id m_our_id;
	bool m_disconnect_called;
	mock_torrent& m_torrent;

#if TORRENT_USE_I2P
	std::string const& destination() const override
	{
		return m_i2p_destination;
	}
	std::string const& local_i2p_endpoint() const override
	{
		return m_local_i2p_endpoint;
	}
#endif

	void get_peer_info(peer_info&) const override {}
	tcp::endpoint const& remote() const override { return m_remote; }
	tcp::endpoint local_endpoint() const override { return m_local; }
	void disconnect(error_code const& ec
		, operation_t op, disconnect_severity_t error = peer_connection_interface::normal) override;
	peer_id const& pid() const override { return m_id; }
	peer_id our_pid() const override { return m_our_id; }
	void set_holepunch_mode() override {}
	torrent_peer* peer_info_struct() const override { return m_tp; }
	void set_peer_info(torrent_peer* pi) override { m_tp = pi; }
	bool is_outgoing() const override { return m_outgoing; }
	void add_stat(std::int64_t downloaded, std::int64_t uploaded) override
	{ m_stat.add_stat(downloaded, uploaded); }
	bool fast_reconnect() const override { return true; }
	bool is_choked() const override { return m_choked; }
	bool failed() const override { return false; }
	aux::stat const& statistics() const override { return m_stat; }
};

struct mock_torrent
{
	explicit mock_torrent(torrent_state* st) : m_p(nullptr), m_state(st) {}
	virtual ~mock_torrent() = default;

	bool connect_to_peer(torrent_peer* peerinfo)
	{
		TORRENT_ASSERT(peerinfo->connection == nullptr);
		if (peerinfo->connection) return false;
		auto c = std::make_shared<mock_peer_connection>(this, true, peerinfo->ip());
		c->set_peer_info(peerinfo);

		m_connections.push_back(c);
		m_p->set_connection(peerinfo, c.get());
		return true;
	}

	peer_list* m_p;
	torrent_state* m_state;
	std::vector<std::shared_ptr<mock_peer_connection>> m_connections;
};

void mock_peer_connection::disconnect(error_code const&
	, operation_t, disconnect_severity_t)
{
	m_torrent.m_p->connection_closed(*this, 0, m_torrent.m_state);
	auto const i = std::find(m_torrent.m_connections.begin(), m_torrent.m_connections.end()
		, std::static_pointer_cast<mock_peer_connection>(shared_from_this()));
	if (i != m_torrent.m_connections.end()) m_torrent.m_connections.erase(i);

	m_tp = nullptr;
	m_disconnect_called = true;
}

bool has_peer(peer_list const& p, tcp::endpoint const& ep)
{
	auto const its = p.find_peers(ep.address());
	return its.first != its.second;
}

torrent_state init_state()
{
	torrent_state st;
	st.is_finished = false;
	st.max_peerlist_size = 1000;
	st.allow_multiple_connections_per_ip = false;
	st.port = 9999;
	return st;
}

torrent_peer* add_peer(peer_list& p, torrent_state& st, tcp::endpoint const& ep)
{
	int cc = p.num_connect_candidates();
	torrent_peer* peer = p.add_peer(ep, {}, {}, &st);
	if (peer)
	{
		TEST_EQUAL(p.num_connect_candidates(), cc + 1);
		TEST_EQUAL(peer->port, ep.port());
	}
	st.erased.clear();
	return peer;
}

#if TORRENT_USE_I2P
torrent_peer* add_i2p_peer(peer_list& p, torrent_state& st, std::string const& destination)
{
	int cc = p.num_connect_candidates();
	torrent_peer* peer = p.add_i2p_peer(destination, {}, {}, &st);
	if (peer)
	{
		TEST_EQUAL(p.num_connect_candidates(), cc + 1);
		TEST_EQUAL(peer->dest(), destination);
	}
	st.erased.clear();
	return peer;
}
#endif

void connect_peer(peer_list& p, mock_torrent& t, torrent_state& st)
{
	torrent_peer* tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp);
	if (!tp) return;
	t.connect_to_peer(tp);
	st.erased.clear();
	TEST_CHECK(tp->connection);
}

static torrent_peer_allocator allocator;

} // anonymous namespace

// test multiple peers with the same IP
// when disallowing it
TORRENT_TEST(multiple_ips_disallowed)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;
	TEST_EQUAL(p.num_connect_candidates(), 0);
	torrent_peer* peer1 = p.add_peer(ep("10.0.0.2", 3000), {}, {}, &st);

	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	st.erased.clear();

	torrent_peer* peer2 = p.add_peer(ep("10.0.0.2", 9020), {}, {}, &st);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(peer1, peer2);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	st.erased.clear();
}

// test multiple peers with the same IP
// when allowing it
TORRENT_TEST(multiple_ips_allowed)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = true;
	peer_list p(allocator);
	t.m_p = &p;
	torrent_peer* peer1 = p.add_peer(ep("10.0.0.2", 3000), {}, {}, &st);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	TEST_EQUAL(p.num_peers(), 1);
	st.erased.clear();

	torrent_peer* peer2 = p.add_peer(ep("10.0.0.2", 9020), {}, {}, &st);
	TEST_EQUAL(p.num_peers(), 2);
	TEST_CHECK(peer1 != peer2);
	TEST_EQUAL(p.num_connect_candidates(), 2);
	st.erased.clear();
}

// test adding two peers with the same IP, but different ports, to
// make sure they can be connected at the same time
// with allow_multiple_connections_per_ip enabled
TORRENT_TEST(multiple_ips_allowed2)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = true;
	peer_list p(allocator);
	t.m_p = &p;
	torrent_peer* peer1 = p.add_peer(ep("10.0.0.2", 3000), {}, {}, &st);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	st.erased.clear();

	TEST_EQUAL(p.num_peers(), 1);
	torrent_peer* tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp);
	t.connect_to_peer(tp);
	st.erased.clear();

	// we only have one peer, we can't
	// connect another one
	tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp == nullptr);
	st.erased.clear();

	torrent_peer* peer2 = p.add_peer(ep("10.0.0.2", 9020), {}, {}, &st);
	TEST_EQUAL(p.num_peers(), 2);
	TEST_CHECK(peer1 != peer2);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	st.erased.clear();

	tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp);
	t.connect_to_peer(tp);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	st.erased.clear();
}

// test adding two peers with the same IP, but different ports, to
// make sure they can not be connected at the same time
// with allow_multiple_connections_per_ip disabled
TORRENT_TEST(multiple_ips_disallowed2)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;
	torrent_peer* peer1 = p.add_peer(ep("10.0.0.2", 3000), {}, {}, &st);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	TEST_EQUAL(peer1->port, 3000);
	st.erased.clear();

	TEST_EQUAL(p.num_peers(), 1);
	torrent_peer* tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp);
	t.connect_to_peer(tp);
	st.erased.clear();

	// we only have one peer, we can't
	// connect another one
	tp = p.connect_one_peer(0, &st);
	TEST_CHECK(tp == nullptr);
	st.erased.clear();

	torrent_peer* peer2 = p.add_peer(ep("10.0.0.2", 9020), {}, {}, &st);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(peer2->port, 9020);
		TEST_CHECK(peer1 == peer2);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	st.erased.clear();
}

// test incoming connection
// and update_peer_port
TORRENT_TEST(update_peer_port)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;
	TEST_EQUAL(p.num_connect_candidates(), 0);
	auto c = std::make_shared<mock_peer_connection>(&t, true, ep("10.0.0.1", 8080));
	p.new_connection(*c, 0, &st);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_peers(), 1);
	st.erased.clear();

	p.update_peer_port(4000, c->peer_info_struct(), peer_info::incoming, &st);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(c->peer_info_struct()->port, 4000);
	st.erased.clear();
}

// test incoming connection
// and update_peer_port, causing collision
TORRENT_TEST(update_peer_port_collide)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = true;
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer2 = p.add_peer(ep("10.0.0.1", 4000), {}, {}, &st);
	TEST_CHECK(peer2);

	TEST_EQUAL(p.num_connect_candidates(), 1);
	auto c = std::make_shared<mock_peer_connection>(&t, true, ep("10.0.0.1", 8080));
	p.new_connection(*c, 0, &st);
	TEST_EQUAL(p.num_connect_candidates(), 1);
	// at this point we have two peers, because we think they have different
	// ports
	TEST_EQUAL(p.num_peers(), 2);
	st.erased.clear();

		// this peer will end up having the same port as the existing peer in the list
	p.update_peer_port(4000, c->peer_info_struct(), peer_info::incoming, &st);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	// the expected behavior is to replace that one
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(c->peer_info_struct()->port, 4000);
	st.erased.clear();
}

namespace {
std::shared_ptr<mock_peer_connection> shared_from_this(lt::peer_connection_interface* p)
{
	return std::static_pointer_cast<mock_peer_connection>(
		static_cast<mock_peer_connection*>(p)->shared_from_this());
}
} // anonymous namespace

// test ip filter
TORRENT_TEST(ip_filter)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// add peer 1
	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.2", 3000));
	torrent_peer* peer2 = add_peer(p, st, ep("11.0.0.2", 9020));

	TEST_CHECK(peer1 != peer2);

	connect_peer(p, t, st);
	connect_peer(p, t, st);

	auto con1 = shared_from_this(peer1->connection);
	TEST_EQUAL(con1->was_disconnected(), false);
	auto con2 = shared_from_this(peer2->connection);
	TEST_EQUAL(con2->was_disconnected(), false);

	// now, filter one of the IPs and make sure the peer is removed
	ip_filter filter;
	filter.add_rule(addr4("11.0.0.0"), addr4("255.255.255.255"), 1);
	std::vector<address> banned;
	p.apply_ip_filter(filter, &st, banned);
	// we just erased a peer, because it was filtered by the ip filter
	TEST_EQUAL(st.erased.size(), 1);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(banned.size(), 1);
	TEST_EQUAL(banned[0], addr4("11.0.0.2"));
	TEST_EQUAL(con2->was_disconnected(), true);
	TEST_EQUAL(con1->was_disconnected(), false);
}

// test port filter
TORRENT_TEST(port_filter)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// add peer 1
	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.2", 3000));
	torrent_peer* peer2 = add_peer(p, st, ep("11.0.0.2", 9020));

	TEST_CHECK(peer1 != peer2);

	connect_peer(p, t, st);
	connect_peer(p, t, st);

	auto con1 = shared_from_this(peer1->connection);
	TEST_EQUAL(con1->was_disconnected(), false);
	auto con2 = shared_from_this(peer2->connection);
	TEST_EQUAL(con2->was_disconnected(), false);

	// now, filter one of the IPs and make sure the peer is removed
	port_filter filter;
	filter.add_rule(9000, 10000, 1);
	std::vector<tcp::endpoint> banned;
	p.apply_port_filter(filter, &st, banned);
	// we just erased a peer, because it was filtered by the ip filter
	TEST_EQUAL(st.erased.size(), 1);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(banned.size(), 1);
	TEST_EQUAL(banned[0], ep("11.0.0.2", 9020));
	TEST_EQUAL(con2->was_disconnected(), true);
	TEST_EQUAL(con1->was_disconnected(), false);
}

// test banning peers
TORRENT_TEST(ban_peers)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.1", 4000));

	TEST_EQUAL(p.num_connect_candidates(), 1);
	auto c = std::make_shared<mock_peer_connection>(&t, true, ep("10.0.0.1", 8080));
	p.new_connection(*c, 0, &st);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_peers(), 1);
	st.erased.clear();

	// now, ban the peer
	bool ok = p.ban_peer(c->peer_info_struct());
	TEST_EQUAL(ok, true);
	TEST_EQUAL(bool(peer1->banned), true);
	// we still have it in the list
	TEST_EQUAL(p.num_peers(), 1);
	// it's just not a connect candidate, nor allowed to receive incoming connections
	TEST_EQUAL(p.num_connect_candidates(), 0);

	p.connection_closed(*c, 0, &st);
	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	st.erased.clear();

	c = std::make_shared<mock_peer_connection>(&t, true, ep("10.0.0.1", 8080));
	ok = p.new_connection(*c, 0, &st);
	// since it's banned, we should not allow this incoming connection
	TEST_EQUAL(ok, false);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	st.erased.clear();
}

// test erase_peers when we fill up the peer list
TORRENT_TEST(erase_peers)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.max_peerlist_size = 100;
	st.allow_multiple_connections_per_ip = true;
	peer_list p(allocator);
	t.m_p = &p;

	for (int i = 0; i < 100; ++i)
	{
		TEST_EQUAL(st.erased.size(), 0);
		tcp::endpoint ep = rand_tcp_ep();
		torrent_peer* peer = add_peer(p, st, ep);
		TEST_CHECK(peer);
		if (peer == nullptr || st.erased.size() > 0)
		{
			std::printf("unexpected rejection of peer: %s | %d in list. "
				"added peer %p, erased %d peers\n"
				, print_endpoint(ep).c_str(), p.num_peers(), static_cast<void*>(peer)
				, int(st.erased.size()));
		}
	}
	TEST_EQUAL(p.num_peers(), 100);

	// trigger the eviction of one peer
	torrent_peer* peer = p.add_peer(rand_tcp_ep(), {}, {}, &st);
	// we either removed an existing peer, or rejected this one
	// either is valid behavior when the list is full
	TEST_CHECK(st.erased.size() == 1 || peer == nullptr);
}

// test set_ip_filter
TORRENT_TEST(set_ip_filter)
{
	torrent_state st = init_state();
	std::vector<address> banned;

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	for (int i = 0; i < 100; ++i)
	{
		p.add_peer(tcp::endpoint(
			address_v4(std::uint32_t((10 << 24) + ((i + 10) << 16))), 353), {}, {}, &st);
		TEST_EQUAL(st.erased.size(), 0);
		st.erased.clear();
	}
	TEST_EQUAL(p.num_peers(), 100);
	TEST_EQUAL(p.num_connect_candidates(), 100);

	// trigger the removal of one peer
	ip_filter filter;
	filter.add_rule(addr4("10.13.0.0"), addr4("10.13.255.255"), ip_filter::blocked);
	p.apply_ip_filter(filter, &st, banned);
	TEST_EQUAL(st.erased.size(), 1);
	TEST_EQUAL(st.erased[0]->address(), addr4("10.13.0.0"));
	TEST_EQUAL(p.num_peers(), 99);
	TEST_EQUAL(p.num_connect_candidates(), 99);
}

// test set_port_filter
TORRENT_TEST(set_port_filter)
{
	torrent_state st = init_state();
	std::vector<tcp::endpoint> banned;

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	for (int i = 0; i < 100; ++i)
	{
		p.add_peer(tcp::endpoint(
			address_v4(std::uint32_t((10 << 24) + ((i + 10) << 16))), std::uint16_t(i + 10)), {}, {}, &st);
		TEST_EQUAL(st.erased.size(), 0);
		st.erased.clear();
	}
	TEST_EQUAL(p.num_peers(), 100);
	TEST_EQUAL(p.num_connect_candidates(), 100);

	// trigger the removal of one peer
	port_filter filter;
	filter.add_rule(13, 13, port_filter::blocked);
	p.apply_port_filter(filter, &st, banned);
	TEST_EQUAL(st.erased.size(), 1);
	TEST_EQUAL(st.erased[0]->address(), addr4("10.13.0.0"));
	TEST_EQUAL(st.erased[0]->port, 13);
	TEST_EQUAL(p.num_peers(), 99);
	TEST_EQUAL(p.num_connect_candidates(), 99);
}

// test set_max_failcount
TORRENT_TEST(set_max_failcount)
{
	torrent_state st = init_state();

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	for (int i = 0; i < 100; ++i)
	{
		torrent_peer* peer = p.add_peer(tcp::endpoint(
			address_v4(std::uint32_t((10 << 24) + ((i + 10) << 16))), std::uint16_t(i + 10)), {}, {}, &st);
		TEST_EQUAL(st.erased.size(), 0);
		st.erased.clear();
		// every other peer has a failcount of 1
		if (i % 2) p.inc_failcount(peer);
	}
	TEST_EQUAL(p.num_peers(), 100);
	TEST_EQUAL(p.num_connect_candidates(), 100);

	// set the max failcount to 1 and observe how half the peers no longer
	// are connect candidates
	st.max_failcount = 1;
	p.set_max_failcount(&st);

	TEST_EQUAL(p.num_connect_candidates(), 50);
	TEST_EQUAL(p.num_peers(), 100);
}

// test set_seed
TORRENT_TEST(set_seed)
{
	torrent_state st = init_state();

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	for (int i = 0; i < 100; ++i)
	{
		torrent_peer* peer = p.add_peer(tcp::endpoint(
			address_v4(std::uint32_t((10 << 24) + ((i + 10) << 16))), std::uint16_t(i + 10)), {}, {}, &st);
		TEST_EQUAL(st.erased.size(), 0);
		st.erased.clear();
		// make every other peer a seed
		if (i % 2) p.set_seed(peer, true);
	}
	TEST_EQUAL(p.num_peers(), 100);
	TEST_EQUAL(p.num_connect_candidates(), 100);

	// now, the torrent completes and we're no longer interested in
	// connecting to seeds. Make sure half the peers are no longer
	// considered connect candidates
	st.is_finished = true;

	// this will make the peer_list recalculate the connect candidates
	std::vector<torrent_peer*> peers;
	p.connect_one_peer(1, &st);

	TEST_EQUAL(p.num_connect_candidates(), 50);
	TEST_EQUAL(p.num_peers(), 100);
}

// test set_upload_only
TORRENT_TEST(set_upload_only)
{
	torrent_state st = init_state();

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	std::vector<torrent_peer*> upload_only_peers;
	for (int i = 0; i < 100; ++i)
	{
		torrent_peer* peer =
			p.add_peer(tcp::endpoint(address_v4(std::uint32_t((10 << 24) + ((i + 10) << 16))),
						   std::uint16_t(i + 10)),
				{},
				{},
				&st);
		TEST_EQUAL(st.erased.size(), 0);
		st.erased.clear();
		// make every other peer upload_only
		if (i % 2)
		{
			p.set_upload_only(peer, true);
			upload_only_peers.push_back(peer);
		}
	}
	TEST_EQUAL(p.num_peers(), 100);
	TEST_EQUAL(p.num_connect_candidates(), 100);

	// now, the torrent completes and we're no longer interested in
	// connecting to upload_only peers. Make sure half the peers are no
	// longer considered connect candidates
	st.is_finished = true;

	// this will make the peer_list recalculate the connect candidates
	p.connect_one_peer(1, &st);

	TEST_EQUAL(p.num_connect_candidates(), 50);
	TEST_EQUAL(p.num_peers(), 100);

	// per BEP-21, the upload_only flag may toggle back to 0. Verify the
	// persistent bit follows and the peers become connect candidates again.
	for (torrent_peer* peer : upload_only_peers)
		p.set_upload_only(peer, false);

	TEST_EQUAL(p.num_connect_candidates(), 100);
	TEST_EQUAL(p.num_peers(), 100);
}

// test has_peer
TORRENT_TEST(has_peer)
{
	torrent_state st = init_state();
	std::vector<address> banned;

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.10.0.1", 10));
	torrent_peer* peer2 = add_peer(p, st, ep("10.10.0.2", 11));

	TEST_EQUAL(p.num_peers(), 2);
	TEST_EQUAL(p.num_connect_candidates(), 2);

	TEST_EQUAL(p.has_peer(peer1), true);
	TEST_EQUAL(p.has_peer(peer2), true);

	ip_filter filter;
	filter.add_rule(addr4("10.10.0.1"), addr4("10.10.0.1"), ip_filter::blocked);
	p.apply_ip_filter(filter, &st, banned);
	TEST_EQUAL(st.erased.size(), 1);
	st.erased.clear();

	TEST_EQUAL(p.num_peers(), 1);
	TEST_EQUAL(p.num_connect_candidates(), 1);

	TEST_EQUAL(p.has_peer(peer1), false);
	TEST_EQUAL(p.has_peer(peer2), true);
}

// test connect_candidates torrent_finish
TORRENT_TEST(connect_candidates_finish)
{
	torrent_state st = init_state();
	std::vector<address> banned;

	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.10.0.1", 10));
	TEST_CHECK(peer1);
	p.set_seed(peer1, true);
	torrent_peer* peer2 = add_peer(p, st, ep("10.10.0.2", 11));
	TEST_CHECK(peer2);
	p.set_seed(peer2, true);
	torrent_peer* peer3 = add_peer(p, st, ep("10.10.0.3", 11));
	TEST_CHECK(peer3);
	p.set_seed(peer3, true);
	torrent_peer* peer4 = add_peer(p, st, ep("10.10.0.4", 11));
	TEST_CHECK(peer4);
	torrent_peer* peer5 = add_peer(p, st, ep("10.10.0.5", 11));
	TEST_CHECK(peer5);

	TEST_EQUAL(p.num_peers(), 5);
	TEST_EQUAL(p.num_connect_candidates(), 5);

	st.is_finished = true;
	// we're finished downloading now, only the non-seeds are
	// connect candidates

	// connect to one of them
	connect_peer(p, t, st);

	TEST_EQUAL(p.num_peers(), 5);
	// and there should be one left
	TEST_EQUAL(p.num_connect_candidates(), 1);
}

// test self-connection
TORRENT_TEST(self_connection)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// add and connect peer
	torrent_peer* peer = add_peer(p, st, ep("10.0.0.2", 3000));
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->set_local_ep(ep("10.0.0.2", 8080));

	auto con_in = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 8080));
	con_in->set_local_ep(ep("10.0.0.2", 3000));

	p.new_connection(*con_in, 0, &st);

	// from the peer_list's point of view, this looks like we made one
	// outgoing connection and received an incoming one. Since they share
	// the exact same endpoints (IP ports) but just swapped source and
	// destination, the peer list is supposed to figure out that we connected
	// to ourself and disconnect it
	TEST_EQUAL(con_out->was_disconnected(), true);
	TEST_EQUAL(con_in->was_disconnected(), true);
}

// test double connection (both incoming)
TORRENT_TEST(double_connection)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are 10.0.0.1 and the other peer is 10.0.0.2

	// first incoming connection
	auto con1 = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 7528));
	con1->set_local_ep(ep("10.0.0.1", 8080));

	p.new_connection(*con1, 0, &st);

	// the seconds incoming connection
	auto con2 = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 3561));
	con2->set_local_ep(ep("10.0.0.1", 8080));

	p.new_connection(*con2, 0, &st);

	// the second incoming connection should be closed
	TEST_EQUAL(con1->was_disconnected(), false);
	TEST_EQUAL(con2->was_disconnected(), true);
}

// test double connection (we loose)
TORRENT_TEST(double_connection_loose)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are 10.0.0.1 and the other peer is 10.0.0.2

	// our outgoing connection
	torrent_peer* peer = add_peer(p, st, ep("10.0.0.2", 3000));
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->set_local_ep(ep("10.0.0.1", 3163));

	// and the incoming connection
	auto con_in = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 3561));
	con_in->set_local_ep(ep("10.0.0.1", 8080));

	p.new_connection(*con_in, 0, &st);

	// the rules are documented in peer_list.cpp
	TEST_EQUAL(con_out->was_disconnected(), true);
	TEST_EQUAL(con_in->was_disconnected(), false);
}

// test double connection with identical ports (random)
TORRENT_TEST(double_connection_random)
{
	int in = 0;
	int out = 0;
	for (int i = 0; i < 30; ++i)
	{
		torrent_state st = init_state();
		mock_torrent t(&st);
		st.allow_multiple_connections_per_ip = false;
		peer_list p(allocator);
		t.m_p = &p;

		// we are 10.0.0.1 and the other peer is 10.0.0.2

		// our outgoing connection
		torrent_peer* peer = add_peer(p, st, ep("10.0.0.2", 3000));
		connect_peer(p, t, st);

		auto con_out = static_cast<mock_peer_connection*>(peer->connection)->shared_from_this();
		con_out->set_local_ep(ep("10.0.0.1", 3000));

		// and the incoming connection
		auto con_in = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 3000));
		con_in->set_local_ep(ep("10.0.0.1", 3000));

		p.new_connection(*con_in, 0, &st);

		// the rules are documented in peer_list.cpp
		out += con_out->was_disconnected();
		in += con_in->was_disconnected();
	}
	// we should have gone different ways randomly
	TEST_CHECK(out > 0);
	TEST_CHECK(in > 0);
}

// test double connection (we win)
TORRENT_TEST(double_connection_win)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are 10.0.0.1 and the other peer is 10.0.0.2

	// our outgoing connection
	torrent_peer* peer = add_peer(p, st, ep("10.0.0.2", 8080));
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->set_local_ep(ep("10.0.0.1", 3163));

	//and the incoming connection
	auto con_in = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.0.2", 3561));
	con_in->set_local_ep(ep("10.0.0.1", 3000));

	p.new_connection(*con_in, 0, &st);

	// the rules are documented in peer_list.cpp
	TEST_EQUAL(con_out->was_disconnected(), false);
	TEST_EQUAL(con_in->was_disconnected(), true);
}

#if TORRENT_USE_I2P
// test i2p self-connection
TORRENT_TEST(self_connection_i2p)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// add and connect peer
	torrent_peer* peer = add_i2p_peer(p, st, "foobar");
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->m_i2p_destination = "foobar";
	con_out->m_local_i2p_endpoint = "foobar";

	auto con_in = std::make_shared<mock_peer_connection>(&t, false, "foobar");
	con_in->m_local_i2p_endpoint = "foobar";

	p.new_connection(*con_in, 0, &st);

	// from the peer_list's point of view, this looks like we made one
	// outgoing connection and received an incoming one. Since they share
	// the exact same endpoints (IP ports) but just swapped source and
	// destination, the peer list is supposed to figure out that we connected
	// to ourself and disconnect it
	TEST_EQUAL(con_out->was_disconnected(), true);
	TEST_EQUAL(con_in->was_disconnected(), true);
}

// test double i2p connection (both incoming)
TORRENT_TEST(double_connection_i2p)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are "foo" and the other peer is "bar"

	// first incoming connection
	auto con1 = std::make_shared<mock_peer_connection>(&t, false, "bar");
	con1->m_local_i2p_endpoint = "foo";

	p.new_connection(*con1, 0, &st);

	// the second incoming connection
	auto con2 = std::make_shared<mock_peer_connection>(&t, false, "bar");
	con2->m_local_i2p_endpoint = "foo";

	p.new_connection(*con2, 0, &st);

	// the second incoming connection should be closed
	TEST_EQUAL(con1->was_disconnected(), false);
	TEST_EQUAL(con2->was_disconnected(), true);
}

// test double connection (we loose)
TORRENT_TEST(double_connection_loose_i2p)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are "foo" and the other peer is "bar"

	// our outgoing connection
	torrent_peer* peer = add_i2p_peer(p, st, "bar");
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->m_local_i2p_endpoint = "foo";

	// and the incoming connection
	auto con_in = std::make_shared<mock_peer_connection>(&t, false, "bar");
	con_in->m_local_i2p_endpoint = "foo";

	p.new_connection(*con_in, 0, &st);

	// the rules are documented in peer_list.cpp
	TEST_EQUAL(con_out->was_disconnected(), true);
	TEST_EQUAL(con_in->was_disconnected(), false);
}

// test double connection (we win)
TORRENT_TEST(double_connection_win_i2p)
{
	torrent_state st = init_state();
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	// we are "bar" and the other peer is "foo"
	// "bar" < "foo", so we gets to make the outgoing connection

	// our outgoing connection
	torrent_peer* peer = add_i2p_peer(p, st, "foo");
	connect_peer(p, t, st);

	auto con_out = shared_from_this(peer->connection);
	con_out->m_local_i2p_endpoint = "bar";

	//and the incoming connection
	auto con_in = std::make_shared<mock_peer_connection>(&t, false, "foo");
	con_in->m_local_i2p_endpoint = "bar";

	p.new_connection(*con_in, 0, &st);

	// the rules are documented in peer_list.cpp
	TEST_EQUAL(con_out->was_disconnected(), true);
	TEST_EQUAL(con_in->was_disconnected(), false);
}
#endif

// test incoming connection when we are at the list size limit
TORRENT_TEST(incoming_size_limit)
{
	torrent_state st = init_state();
	st.max_peerlist_size = 5;
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.1", 8080));
	TEST_CHECK(peer1);
	TEST_EQUAL(p.num_peers(), 1);
	torrent_peer* peer2 = add_peer(p, st, ep("10.0.0.2", 8080));
	TEST_CHECK(peer2);
	TEST_EQUAL(p.num_peers(), 2);
	torrent_peer* peer3 = add_peer(p, st, ep("10.0.0.3", 8080));
	TEST_CHECK(peer3);
	TEST_EQUAL(p.num_peers(), 3);
	torrent_peer* peer4 = add_peer(p, st, ep("10.0.0.4", 8080));
	TEST_CHECK(peer4);
	TEST_EQUAL(p.num_peers(), 4);
	torrent_peer* peer5 = add_peer(p, st, ep("10.0.0.5", 8080));
	TEST_CHECK(peer5);
	TEST_EQUAL(p.num_peers(), 5);

	auto con_in = std::make_shared<mock_peer_connection>(&t, false, ep("10.0.1.2", 3561));
	con_in->set_local_ep(ep("10.0.2.1", 3000));

	// since we're already at 5 peers in the peer list, this call should
	// erase one of the existing ones.
	p.new_connection(*con_in, 0, &st);

	TEST_EQUAL(con_in->was_disconnected(), false);
	TEST_EQUAL(p.num_peers(), 5);

	// one of the previous ones should have been removed
	TEST_EQUAL(has_peer(p, ep("10.0.0.1", 8080))
		+ has_peer(p, ep("10.0.0.2", 8080))
		+ has_peer(p, ep("10.0.0.3", 8080))
		+ has_peer(p, ep("10.0.0.4", 8080))
		+ has_peer(p, ep("10.0.0.5", 8080))
		, 4);
}

// test new peer when we are at the list size limit
TORRENT_TEST(new_peer_size_limit)
{
	torrent_state st = init_state();
	st.max_peerlist_size = 5;
	mock_torrent t(&st);
	st.allow_multiple_connections_per_ip = false;
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.1", 8080));
	TEST_CHECK(peer1);
	TEST_EQUAL(p.num_peers(), 1);
	torrent_peer* peer2 = add_peer(p, st, ep("10.0.0.2", 8080));
	TEST_CHECK(peer2);
	TEST_EQUAL(p.num_peers(), 2);
	torrent_peer* peer3 = add_peer(p, st, ep("10.0.0.3", 8080));
	TEST_CHECK(peer3);
	TEST_EQUAL(p.num_peers(), 3);
	torrent_peer* peer4 = add_peer(p, st, ep("10.0.0.4", 8080));
	TEST_CHECK(peer4);
	TEST_EQUAL(p.num_peers(), 4);
	torrent_peer* peer5 = add_peer(p, st, ep("10.0.0.5", 8080));
	TEST_CHECK(peer5);
	TEST_EQUAL(p.num_peers(), 5);
	torrent_peer* peer6 = p.add_peer(ep("10.0.0.6", 8080), {}, {}, &st);
	TEST_CHECK(peer6 == nullptr);
	TEST_EQUAL(p.num_peers(), 5);

	// one of the connection should have been removed
	TEST_EQUAL(has_peer(p, ep("10.0.0.1", 8080))
		+ has_peer(p, ep("10.0.0.2", 8080))
		+ has_peer(p, ep("10.0.0.3", 8080))
		+ has_peer(p, ep("10.0.0.4", 8080))
		+ has_peer(p, ep("10.0.0.5", 8080))
		+ has_peer(p, ep("10.0.0.6", 8080))
		, 5);
}

TORRENT_TEST(peer_info_comparison)
{
	peer_address_compare cmp;
	ipv4_peer const ip_low(ep("1.1.1.1", 6888), true, {});
	ipv4_peer const ip_high(ep("100.1.1.1", 6888), true, {});
	TEST_CHECK(cmp(&ip_low, &ip_high));
	TEST_CHECK(!cmp(&ip_high, &ip_low));
	TEST_CHECK(!cmp(&ip_high, &ip_high));
	TEST_CHECK(!cmp(&ip_low, &ip_low));

	TEST_CHECK(!cmp(&ip_low, addr("1.1.1.1")));
	TEST_CHECK(cmp(&ip_low, addr("1.1.1.2")));
	TEST_CHECK(cmp(&ip_low, addr("100.1.1.1")));

	TEST_CHECK(!cmp(addr("1.1.1.1"), &ip_low));
	TEST_CHECK(cmp(addr("1.1.1.0"), &ip_low));
	TEST_CHECK(!cmp( addr("100.1.1.1"), &ip_low));

#if TORRENT_USE_I2P
	i2p_peer const i2p_low("aaaaaa", true, {});
	i2p_peer const i2p_high("zzzzzz", true, {});

	// noremal IPs always sort before i2p addresses
	TEST_CHECK(cmp(&ip_low, &i2p_high));
	TEST_CHECK(cmp(&ip_low, &i2p_low));
	TEST_CHECK(cmp(&ip_high, &i2p_high));
	TEST_CHECK(cmp(&ip_high, &i2p_low));

	// i2p addresses always sort after noremal IPs
	TEST_CHECK(!cmp(&i2p_high, &ip_low));
	TEST_CHECK(!cmp(&i2p_low, &ip_low));
	TEST_CHECK(!cmp(&i2p_high, &ip_high));
	TEST_CHECK(!cmp(&i2p_low, &ip_high));

	// noremal IPs always sort before i2p addresses
	TEST_CHECK(cmp(addr4("1.1.1.1"), &i2p_high));
	TEST_CHECK(cmp(addr4("1.1.1.1"), &i2p_low));
	TEST_CHECK(cmp(addr4("100.1.1.1"), &i2p_high));
	TEST_CHECK(cmp(addr4("100.1.1.1"), &i2p_low));

	// i2p addresses always sort after noremal IPs
	TEST_CHECK(!cmp(&i2p_high, addr4("1.1.1.1")));
	TEST_CHECK(!cmp(&i2p_low, addr4("1.1.1.1")));
	TEST_CHECK(!cmp(&i2p_high, addr4("100.1.1.1")));
	TEST_CHECK(!cmp(&i2p_low, addr4("100.1.1.1")));

	// internal i2p sorting
	TEST_CHECK(cmp(&i2p_low, &i2p_high));
	TEST_CHECK(!cmp(&i2p_high, &i2p_low));
	TEST_CHECK(!cmp(&i2p_high, &i2p_high));
	TEST_CHECK(!cmp(&i2p_low, &i2p_low));

	TEST_CHECK(cmp(&i2p_low, "zzzzzz"));
	TEST_CHECK(!cmp(&i2p_high, "aaaaaa"));
	TEST_CHECK(!cmp(&i2p_high, "zzzzzz"));
	TEST_CHECK(!cmp(&i2p_low, "aaaaaa"));
	TEST_CHECK(cmp(&i2p_low, "aaaaab"));

	TEST_CHECK(cmp("aaaaaa", &i2p_high));
	TEST_CHECK(!cmp("zzzzzz", &i2p_low));
	TEST_CHECK(!cmp("zzzzzz", &i2p_high));
	TEST_CHECK(cmp("zzzzzy", &i2p_high));
	TEST_CHECK(!cmp("aaaaaa", &i2p_low));
#endif
}

#if TORRENT_USE_I2P
TORRENT_TEST(peer_info_set_i2p_destination)
{
	peer_info p;
	TORRENT_ASSERT(!(p.flags & peer_info::i2p_socket));
	p.set_i2p_destination(sha256_hash("................................"));

	TORRENT_ASSERT(p.i2p_destination() == sha256_hash("................................"));
	TORRENT_ASSERT(p.flags & peer_info::i2p_socket);
}
#endif

TORRENT_TEST(clear_peers)
{
	torrent_state st = init_state();
	st.max_peerlist_size = 5;
	mock_torrent t(&st);
	peer_list p(allocator);
	t.m_p = &p;

	torrent_peer* peer1 = add_peer(p, st, ep("10.0.0.1", 8080));
	TEST_CHECK(peer1);
	p.set_seed(peer1, true);

	torrent_peer* peer2 = add_peer(p, st, ep("10.0.0.2", 8080));
	TEST_CHECK(peer2);

	torrent_peer* peer3 = add_peer(p, st, ep("10.0.0.3", 8080));
	TEST_CHECK(peer3);

	p.connect_one_peer(1, &st);

	TEST_EQUAL(p.num_peers(), 3);
	TEST_EQUAL(p.num_candidate_cache(), 2);
	TEST_EQUAL(p.num_connect_candidates(), 3);
	TEST_EQUAL(p.num_seeds(), 1);

	p.clear();
	TEST_EQUAL(p.num_peers(), 0);
	TEST_EQUAL(p.num_candidate_cache(), 0);
	TEST_EQUAL(p.num_connect_candidates(), 0);
	TEST_EQUAL(p.num_seeds(), 0);
}

// TODO: test erasing peers
// TODO: test update_peer_port with allow_multiple_connections_per_ip and without
// TODO: test add i2p peers
// TODO: test allow_i2p_mixed
// TODO: test insert_peer failing with all error conditions
// TODO: test IPv6
// TODO: test connect_to_peer() failing
// TODO: test connection_closed
// TODO: connect candidates recalculation when incrementing failcount
