//
// ReplicaSetTest.cpp
//
// Copyright (c) 2012-2025, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier:	BSL-1.0
//


#include "ReplicaSetTest.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/TestSuite.h"
#include "Poco/MongoDB/ServerDescription.h"
#include "Poco/MongoDB/TopologyDescription.h"
#include "Poco/MongoDB/ReadPreference.h"
#include "Poco/MongoDB/ReplicaSet.h"
#include "Poco/MongoDB/ReplicaSetURI.h"
#include "Poco/MongoDB/Document.h"
#include "Poco/MongoDB/Array.h"
#include <chrono>
#include <limits>
#include "Poco/Exception.h"
#include "Poco/MongoDB/Connection.h"
#include "Poco/MongoDB/OpMsgMessage.h"
#include "Poco/MongoDB/PoolableConnectionFactory.h"
#include "Poco/MongoDB/ReplicaSetConnection.h"
#include "Poco/MongoDB/ReplicaSetPoolableConnectionFactory.h"
#include "Poco/Net/SocketAddress.h"
#include "Poco/ObjectPool.h"
#include "Poco/Timespan.h"

using namespace Poco::MongoDB;
using namespace Poco::Net;
using namespace std::string_literals;


ReplicaSetTest::ReplicaSetTest(const std::string& name):
	CppUnit::TestCase(name)
{
}


ReplicaSetTest::~ReplicaSetTest()
{
}


void ReplicaSetTest::setUp()
{
}


void ReplicaSetTest::tearDown()
{
}


// Generic helper function to create hello response documents with configurable parameters
Document::Ptr createHelloResponse(
	bool isWritablePrimary,
	bool isSecondary,
	const std::string& setName = "",
	const std::string& me = "",
	const std::vector<std::string>& hosts = {},
	const std::vector<std::string>& arbiters = {},
	const std::vector<std::string>& passives = {},
	Document::Ptr tags = nullptr,
	bool arbiterOnly = false,
	bool hidden = false,
	const std::string& msg = "")
{
	Document::Ptr doc = new Document();

	doc->add("isWritablePrimary"s, isWritablePrimary);
	doc->add("secondary"s, isSecondary);

	if (arbiterOnly)
	{
		doc->add("arbiterOnly"s, true);
	}

	if (hidden)
	{
		doc->add("hidden"s, true);
	}

	if (!setName.empty())
	{
		doc->add("setName"s, setName);
	}

	if (!me.empty())
	{
		doc->add("me"s, me);
	}

	if (!hosts.empty())
	{
		Array::Ptr hostsArray = new Array();
		for (const auto& host : hosts)
		{
			hostsArray->add(host);
		}
		doc->add("hosts"s, hostsArray);
	}

	if (!arbiters.empty())
	{
		Array::Ptr arbitersArray = new Array();
		for (const auto& arbiter : arbiters)
		{
			arbitersArray->add(arbiter);
		}
		doc->add("arbiters"s, arbitersArray);
	}

	if (!passives.empty())
	{
		Array::Ptr passivesArray = new Array();
		for (const auto& passive : passives)
		{
			passivesArray->add(passive);
		}
		doc->add("passives"s, passivesArray);
	}

	if (tags)
	{
		doc->add("tags"s, tags);
	}

	if (!msg.empty())
	{
		doc->add("msg"s, msg);
	}

	doc->add("ok"s, 1.0);

	return doc;
}


// Convenience function to create a hello response document for a primary server
Document::Ptr createPrimaryHelloResponse()
{
	return createHelloResponse(
		true,  // isWritablePrimary
		false, // isSecondary
		"rs0"s,
		"localhost:27017"s,
		{"localhost:27017"s, "localhost:27018"s, "localhost:27019"s}
	);
}


// Convenience function to create a hello response document for a secondary server
Document::Ptr createSecondaryHelloResponse()
{
	return createHelloResponse(
		false, // isWritablePrimary
		true,  // isSecondary
		"rs0"s,
		"localhost:27018"s,
		{"localhost:27017"s, "localhost:27018"s, "localhost:27019"s}
	);
}


// Convenience function to create a hello response document for an arbiter
Document::Ptr createArbiterHelloResponse()
{
	return createHelloResponse(
		false, // isWritablePrimary
		false, // isSecondary
		"rs0"s,
		"localhost:27019"s,
		{"localhost:27017"s, "localhost:27018"s},
		{"localhost:27019"s}, // arbiters
		{},                   // passives
		nullptr,              // tags
		true                  // arbiterOnly
	);
}


// Convenience function to create a hello response document for a standalone server
Document::Ptr createStandaloneHelloResponse()
{
	return createHelloResponse(
		true,  // isWritablePrimary
		false  // isSecondary
		// No setName - indicates standalone
	);
}


// Convenience function to create a hello response document for a mongos router
Document::Ptr createMongosHelloResponse()
{
	return createHelloResponse(
		true,  // isWritablePrimary
		false, // isSecondary
		"",    // setName
		"",    // me
		{},    // hosts
		{},    // arbiters
		{},    // passives
		nullptr, // tags
		false, // arbiterOnly
		false, // hidden
		"isdbgrid"s // msg - identifies mongos
	);
}


// Convenience function to create a hello response with tags
Document::Ptr createTaggedSecondaryHelloResponse()
{
	Document::Ptr tags = new Document();
	tags->add("dc"s, "east"s);
	tags->add("rack"s, "1"s);
	tags->add("size"s, "large"s);

	return createHelloResponse(
		false, // isWritablePrimary
		true,  // isSecondary
		"rs0"s,
		"localhost:27018"s,
		{"localhost:27017"s, "localhost:27018"s},
		{},    // arbiters
		{},    // passives
		tags
	);
}


// Convenience function to create a hello response with hidden member
Document::Ptr createHiddenMemberHelloResponse()
{
	return createHelloResponse(
		false, // isWritablePrimary
		false, // isSecondary
		"rs0"s,
		"localhost:27020"s,
		{"localhost:27017"s, "localhost:27018"s},
		{},    // arbiters
		{"localhost:27020"s}, // passives
		nullptr, // tags
		false, // arbiterOnly
		true   // hidden
	);
}


void ReplicaSetTest::testServerDescriptionPrimary()
{
	SocketAddress addr("localhost:27017");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createPrimaryHelloResponse();
	auto hosts = server.updateFromHelloResponse(*helloResponse, 5000);  // 5ms RTT

	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(server.type()));
	assertEqual("rs0"s, server.setName());
	assertEqual(5000, server.roundTripTime());
	assertTrue(server.isPrimary());
	assertTrue(server.isWritable());
	assertFalse(server.isSecondary());
	assertFalse(server.hasError());

	// Check hosts list
	assertEqual(3, static_cast<int>(hosts.size()));
	if (addr.family() == SocketAddress::IPv6)
	{
		// Adjust for IPv6 format
		assertEqual("[::1]:27017"s, hosts[0].toString());
		assertEqual("[::1]:27018"s, hosts[1].toString());
		assertEqual("[::1]:27019"s, hosts[2].toString());
	}
	else
	{
		assertEqual("localhost:27017"s, hosts[0].toString());
		assertEqual("localhost:27018"s, hosts[1].toString());
		assertEqual("localhost:27019"s, hosts[2].toString());
	}
}


void ReplicaSetTest::testServerDescriptionSecondary()
{
	SocketAddress addr("localhost:27018");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createSecondaryHelloResponse();
	auto hosts = server.updateFromHelloResponse(*helloResponse, 3000);  // 3ms RTT

	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(server.type()));
	assertEqual("rs0"s, server.setName());
	assertEqual(3000, server.roundTripTime());
	assertFalse(server.isPrimary());
	assertFalse(server.isWritable());
	assertTrue(server.isSecondary());
	assertFalse(server.hasError());

	// Check hosts list
	assertEqual(3, static_cast<int>(hosts.size()));
}


void ReplicaSetTest::testServerDescriptionArbiter()
{
	SocketAddress addr("localhost:27019");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createArbiterHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 2000);  // 2ms RTT

	assertEqual(static_cast<int>(ServerDescription::RsArbiter), static_cast<int>(server.type()));
	assertEqual("rs0"s, server.setName());
	assertEqual(2000, server.roundTripTime());
	assertFalse(server.isPrimary());
	assertFalse(server.isWritable());
	assertFalse(server.isSecondary());
	assertFalse(server.hasError());
}


void ReplicaSetTest::testServerDescriptionStandalone()
{
	SocketAddress addr("localhost:27017");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createStandaloneHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 1000);  // 1ms RTT

	assertEqual(static_cast<int>(ServerDescription::Standalone), static_cast<int>(server.type()));
	assertTrue(server.setName().empty());  // No replica set name for standalone
	assertTrue(server.isPrimary());  // Standalone treated as primary for read preferences
	assertTrue(server.isWritable());
	assertFalse(server.isSecondary());
	assertFalse(server.hasError());
}


void ReplicaSetTest::testServerDescriptionMongos()
{
	SocketAddress addr("localhost:27017");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createMongosHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 4000);  // 4ms RTT

	assertEqual(static_cast<int>(ServerDescription::Mongos), static_cast<int>(server.type()));
	assertTrue(server.setName().empty());  // Mongos doesn't have a set name
	assertFalse(server.isPrimary());
	assertFalse(server.isWritable());
	assertFalse(server.isSecondary());
	assertFalse(server.hasError());
}


void ReplicaSetTest::testServerDescriptionWithTags()
{
	SocketAddress addr("localhost:27018");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createTaggedSecondaryHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 7000);  // 7ms RTT

	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(server.type()));

	// Verify tags are parsed correctly
	const Document& tags = server.tags();
	assertFalse(tags.empty());
	assertEqual(3, static_cast<int>(tags.size()));
	assertEqual("east"s, tags.get<std::string>("dc"s));
	assertEqual("1"s, tags.get<std::string>("rack"s));
	assertEqual("large"s, tags.get<std::string>("size"s));
}


void ReplicaSetTest::testServerDescriptionWithHosts()
{
	SocketAddress addr("localhost:27020");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createHiddenMemberHelloResponse();
	auto hosts = server.updateFromHelloResponse(*helloResponse, 8000);  // 8ms RTT

	assertEqual(static_cast<int>(ServerDescription::RsOther), static_cast<int>(server.type()));

	// Verify hosts list includes regular hosts + passives
	assertEqual(3, static_cast<int>(hosts.size()));  // 2 regular + 1 passive

	// Check that passive member is included
	bool foundPassive = false;
	for (const auto& host : hosts)
	{
		if (host.toString() == "localhost:27020"s || host.toString() == "[::1]:27020"s)
		{
			foundPassive = true;
			break;
		}
	}
	assertTrue(foundPassive);
}


void ReplicaSetTest::testServerDescriptionErrorHandling()
{
	SocketAddress addr("localhost:27017");
	ServerDescription server(addr);

	// Initially update from a valid hello response
	Document::Ptr helloResponse = createPrimaryHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 5000);
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(server.type()));
	assertFalse(server.hasError());

	// Now mark it as having an error
	server.markError("Connection timeout");

	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(server.type()));
	assertTrue(server.hasError());
	assertEqual("Connection timeout"s, server.error());
	assertFalse(server.isPrimary());
	assertFalse(server.isWritable());
}


void ReplicaSetTest::testServerDescriptionReset()
{
	SocketAddress addr("localhost:27017");
	ServerDescription server(addr);

	Document::Ptr helloResponse = createPrimaryHelloResponse();
	(void)server.updateFromHelloResponse(*helloResponse, 5000);

	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(server.type()));
	assertEqual("rs0"s, server.setName());

	// Reset the server description
	server.reset();

	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(server.type()));
	assertTrue(server.setName().empty());
	assertEqual(std::numeric_limits<Poco::Int64>::max(), server.roundTripTime());
	assertTrue(server.tags().empty());
	assertFalse(server.hasError());
	assertFalse(server.isPrimary());
	assertFalse(server.isSecondary());
}


// ============================================================================
// TopologyDescription Tests
// ============================================================================


void ReplicaSetTest::testTopologyEmpty()
{
	TopologyDescription topology;

	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertTrue(topology.setName().empty());
	assertEqual(0, static_cast<int>(topology.serverCount()));
	assertFalse(topology.hasPrimary());
	assertTrue(topology.servers().empty());
	assertTrue(topology.findSecondaries().empty());
}


void ReplicaSetTest::testTopologyAddServers()
{
	TopologyDescription topology("rs0"s);

	assertEqual("rs0"s, topology.setName());
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Add first server
	SocketAddress addr1("localhost:27017");
	topology.addServer(addr1);

	assertEqual(1, static_cast<int>(topology.serverCount()));
	assertTrue(topology.hasServer(addr1));

	// Add second server
	SocketAddress addr2("localhost:27018");
	topology.addServer(addr2);

	assertEqual(2, static_cast<int>(topology.serverCount()));
	assertTrue(topology.hasServer(addr2));

	// Adding same server again should be no-op
	topology.addServer(addr1);
	assertEqual(2, static_cast<int>(topology.serverCount()));
}


void ReplicaSetTest::testTopologyUpdateToPrimary()
{
	TopologyDescription topology;

	SocketAddress addr("localhost:27017");
	Document::Ptr helloResponse = createPrimaryHelloResponse();

	topology.updateServer(addr, *helloResponse, 5000);

	// Should discover it's a replica set with primary
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());
	assertTrue(topology.hasPrimary());

	ServerDescription primary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(primary.type()));
	if (addr.family() == SocketAddress::IPv6)
	{
		assertEqual("[::1]:27017"s, primary.address().toString());
	}
	else
	{
		assertEqual("localhost:27017"s, primary.address().toString());
	}
}


void ReplicaSetTest::testTopologyUpdateToSecondary()
{
	TopologyDescription topology("rs0"s);

	SocketAddress addr("localhost:27018");
	Document::Ptr helloResponse = createSecondaryHelloResponse();

	topology.updateServer(addr, *helloResponse, 3000);

	// Replica set without primary (only secondary known)
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());
	assertFalse(topology.hasPrimary());

	auto secondaries = topology.findSecondaries();
	assertEqual(1, static_cast<int>(secondaries.size()));
	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(secondaries[0].type()));
}


void ReplicaSetTest::testTopologyReplicaSetWithPrimary()
{
	TopologyDescription topology;

	// Add primary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	// Add secondary
	SocketAddress secondary("localhost:27018");
	topology.updateServer(secondary, *createSecondaryHelloResponse(), 3000);

	// Add arbiter
	SocketAddress arbiter("localhost:27019");
	topology.updateServer(arbiter, *createArbiterHelloResponse(), 2000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());
	assertTrue(topology.hasPrimary());

	// Verify we can find primary
	ServerDescription primaryServer = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(primaryServer.type()));

	if (primary.family() == SocketAddress::IPv6)
	{
		assertEqual("[::1]:27017"s, primaryServer.address().toString());
	}
	else
	{
		assertEqual("localhost:27017"s, primaryServer.address().toString());
	}

	// Verify we can find secondaries
	auto secondaries = topology.findSecondaries();
	assertEqual(1, static_cast<int>(secondaries.size()));
	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(secondaries[0].type()));
}


void ReplicaSetTest::testTopologyReplicaSetNoPrimary()
{
	TopologyDescription topology("rs0"s);

	// Add only secondary servers (no primary)
	SocketAddress secondary1("localhost:27018");
	topology.updateServer(secondary1, *createSecondaryHelloResponse(), 3000);

	// Create another secondary response for different server
	Document::Ptr secondary2Response = new Document();
	secondary2Response->add("isWritablePrimary"s, false);
	secondary2Response->add("secondary"s, true);
	secondary2Response->add("setName"s, "rs0"s);
	secondary2Response->add("me"s, "localhost:27019"s);
	Array::Ptr hosts = new Array();
	hosts->add("localhost:27018"s);
	hosts->add("localhost:27019"s);
	secondary2Response->add("hosts"s, hosts);
	secondary2Response->add("ok"s, 1.0);

	SocketAddress secondary2("localhost:27019");
	topology.updateServer(secondary2, *secondary2Response, 4000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());
	assertFalse(topology.hasPrimary());

	auto secondaries = topology.findSecondaries();
	assertEqual(2, static_cast<int>(secondaries.size()));
}


void ReplicaSetTest::testTopologyStandalone()
{
	TopologyDescription topology;

	SocketAddress addr("localhost:27017");
	Document::Ptr helloResponse = createStandaloneHelloResponse();

	topology.updateServer(addr, *helloResponse, 1000);

	// Single standalone server
	assertEqual(static_cast<int>(TopologyDescription::Single), static_cast<int>(topology.type()));
	assertTrue(topology.setName().empty());
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Standalone servers are treated as primary for read preferences
	assertTrue(topology.hasPrimary());
	ServerDescription primary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::Standalone), static_cast<int>(primary.type()));
}


void ReplicaSetTest::testTopologySharded()
{
	TopologyDescription topology;

	// Add mongos routers
	SocketAddress mongos1("localhost:27017");
	topology.updateServer(mongos1, *createMongosHelloResponse(), 5000);

	SocketAddress mongos2("localhost:27018");
	topology.updateServer(mongos2, *createMongosHelloResponse(), 6000);

	assertEqual(static_cast<int>(TopologyDescription::Sharded), static_cast<int>(topology.type()));
	assertEqual(2, static_cast<int>(topology.serverCount()));
	assertTrue(topology.setName().empty());

	// Sharded topology doesn't have primary/secondaries
	assertFalse(topology.hasPrimary());
	assertTrue(topology.findSecondaries().empty());
}


void ReplicaSetTest::testTopologyFindPrimary()
{
	TopologyDescription topology;

	// Initially no primary
	ServerDescription noPrimary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(noPrimary.type()));

	// Add secondary first
	SocketAddress secondary("localhost:27018");
	topology.updateServer(secondary, *createSecondaryHelloResponse(), 3000);

	// Still no primary
	noPrimary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(noPrimary.type()));

	// Add primary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	// Now we should find the primary
	ServerDescription foundPrimary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(foundPrimary.type()));
	if (primary.family() == SocketAddress::IPv6)
	{
		assertEqual("[::1]:27017"s, foundPrimary.address().toString());
	}
	else
	{
		assertEqual("localhost:27017"s, foundPrimary.address().toString());
	}
}


void ReplicaSetTest::testTopologyFindSecondaries()
{
	TopologyDescription topology;

	// Initially no secondaries
	auto secondaries = topology.findSecondaries();
	assertTrue(secondaries.empty());

	// Add primary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	// Still no secondaries
	secondaries = topology.findSecondaries();
	assertTrue(secondaries.empty());

	// Add first secondary
	SocketAddress secondary1("localhost:27018");
	topology.updateServer(secondary1, *createSecondaryHelloResponse(), 3000);

	secondaries = topology.findSecondaries();
	assertEqual(1, static_cast<int>(secondaries.size()));

	// Add tagged secondary (which is also a secondary)
	SocketAddress secondary2("localhost:27020");
	topology.updateServer(secondary2, *createTaggedSecondaryHelloResponse(), 4000);

	secondaries = topology.findSecondaries();
	assertEqual(2, static_cast<int>(secondaries.size()));
}


void ReplicaSetTest::testTopologyMarkServerUnknown()
{
	TopologyDescription topology;

	// Add primary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));
	assertTrue(topology.hasPrimary());

	// Mark primary as unknown (simulating connection failure)
	topology.markServerUnknown(primary, "Connection failed");

	// Topology should transition to no primary
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertFalse(topology.hasPrimary());

	// Server should still exist but be Unknown
	ServerDescription server = topology.getServer(primary);
	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(server.type()));
}


void ReplicaSetTest::testTopologyRemoveServer()
{
	TopologyDescription topology;

	SocketAddress addr1("localhost:27017");
	SocketAddress addr2("localhost:27018");

	topology.addServer(addr1);
	topology.addServer(addr2);
	assertEqual(2, static_cast<int>(topology.serverCount()));

	topology.removeServer(addr1);
	assertEqual(1, static_cast<int>(topology.serverCount()));
	assertFalse(topology.hasServer(addr1));
	assertTrue(topology.hasServer(addr2));

	topology.removeServer(addr2);
	assertEqual(0, static_cast<int>(topology.serverCount()));
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
}


void ReplicaSetTest::testTopologyDiscoverNewHosts()
{
	TopologyDescription topology;

	assertEqual(0, static_cast<int>(topology.serverCount()));

	// Update with primary - hello response includes hosts array
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	// Topology should have discovered all hosts from the hello response
	// Primary response includes: localhost:27017, localhost:27018, localhost:27019
	assertEqual(3, static_cast<int>(topology.serverCount()));

	if (primary.family() == SocketAddress::IPv6)
	{
		assertTrue(topology.hasServer(SocketAddress("[::1]:27017"s)));
		assertTrue(topology.hasServer(SocketAddress("[::1]:27018"s)));
		assertTrue(topology.hasServer(SocketAddress("[::1]:27019"s)));
	}
	else
	{
		assertTrue(topology.hasServer(SocketAddress("localhost:27017"s)));
		assertTrue(topology.hasServer(SocketAddress("localhost:27018"s)));
		assertTrue(topology.hasServer(SocketAddress("localhost:27019"s)));
	}
}


void ReplicaSetTest::testTopologySetNameMismatch()
{
	TopologyDescription topology("rs0"s);

	assertEqual("rs0"s, topology.setName());

	// Create hello response with different set name and discovered hosts
	Document::Ptr wrongSetResponse = new Document();
	wrongSetResponse->add("isWritablePrimary"s, true);
	wrongSetResponse->add("secondary"s, false);
	wrongSetResponse->add("setName"s, "differentSet"s);  // Wrong set name
	wrongSetResponse->add("ok"s, 1.0);

	// Add hosts array - these should NOT be added to topology due to set name mismatch
	Array::Ptr hosts = new Array();
	hosts->add<std::string>("localhost:27018"s);
	hosts->add<std::string>("localhost:27019"s);
	wrongSetResponse->add("hosts"s, hosts);

	SocketAddress addr("localhost:27017");

	// Update server with mismatched set name
	const ServerDescription& server = topology.updateServer(addr, *wrongSetResponse, 5000);

	// Original set name is preserved (set in constructor)
	assertEqual("rs0"s, topology.setName());

	// Server is added to topology
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Server should be marked as Unknown due to set name mismatch
	assertEqual(static_cast<int>(ServerDescription::Unknown), static_cast<int>(server.type()));
	assertTrue(server.hasError());
	assertTrue(server.error().find("Replica set name mismatch") != std::string::npos);
	assertTrue(server.error().find("expected 'rs0'") != std::string::npos);
	assertTrue(server.error().find("but server reports 'differentSet'") != std::string::npos);

	// Discovered hosts from mismatched server should NOT be added (prevent cross-contamination)
	// Only the queried server itself should be in topology
	assertEqual(1, static_cast<int>(topology.serverCount()));
}


void ReplicaSetTest::testTopologyMixedUnknownAndKnown()
{
	TopologyDescription topology("rs0"s);

	// Add a primary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));

	// Add an unknown server (by marking it with error)
	SocketAddress unknown("localhost:27020");
	topology.addServer(unknown);
	topology.markServerUnknown(unknown, "Connection timeout");

	// Topology should still be ReplicaSetWithPrimary (unknown servers don't affect classification)
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));
	assertEqual(4, static_cast<int>(topology.serverCount()));  // primary + 2 discovered from hello + 1 unknown

	// Verify primary is still findable
	assertTrue(topology.hasPrimary());
	ServerDescription foundPrimary = topology.findPrimary();
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(foundPrimary.type()));

	// Mark primary as unknown
	topology.markServerUnknown(primary, "Connection failed");

	// Should transition to ReplicaSetNoPrimary (we have setName and other known members)
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertFalse(topology.hasPrimary());
}


void ReplicaSetTest::testTopologyAllUnknown()
{
	TopologyDescription topology;

	// Add multiple unknown servers
	SocketAddress addr1("localhost:27017");
	SocketAddress addr2("localhost:27018");
	SocketAddress addr3("localhost:27019");

	topology.addServer(addr1);
	topology.addServer(addr2);
	topology.addServer(addr3);

	// All servers are unknown (not yet updated), topology should be Unknown
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertEqual(3, static_cast<int>(topology.serverCount()));

	// Mark all servers with errors
	topology.markServerUnknown(addr1, "Timeout");
	topology.markServerUnknown(addr2, "Timeout");
	topology.markServerUnknown(addr3, "Timeout");

	// Still Unknown topology (no known server types)
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertFalse(topology.hasPrimary());
	assertTrue(topology.findSecondaries().empty());
}


void ReplicaSetTest::testTopologyMultipleStandalone()
{
	TopologyDescription topology;

	// Add first standalone server
	SocketAddress addr1("localhost:27017");
	topology.updateServer(addr1, *createStandaloneHelloResponse(), 1000);

	// Single standalone should be "Single" topology
	assertEqual(static_cast<int>(TopologyDescription::Single), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add second standalone server
	SocketAddress addr2("localhost:27018");
	topology.updateServer(addr2, *createStandaloneHelloResponse(), 2000);

	// Multiple standalone servers should result in Unknown topology
	// (standalone servers shouldn't be in a multi-server configuration)
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertEqual(2, static_cast<int>(topology.serverCount()));
}


void ReplicaSetTest::testTopologyMixedMongosAndPrimary()
{
	TopologyDescription topology;

	// Add a mongos server
	SocketAddress mongosAddr("localhost:27017");
	topology.updateServer(mongosAddr, *createMongosHelloResponse(), 1000);

	// Should be Sharded topology with one mongos
	assertEqual(static_cast<int>(TopologyDescription::Sharded), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add a replica set primary (incompatible!)
	SocketAddress primaryAddr("localhost:27018");
	topology.updateServer(primaryAddr, *createPrimaryHelloResponse(), 2000);

	// Mixed mongos + replica set primary should result in Unknown topology
	// Cannot mix sharded and replica set topologies
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Server count should be >= 2 (the two servers we queried, plus any discovered hosts)
	// Note: Discovered hosts from primary's hello response are added even when types are incompatible
	// This preserves diagnostic information - we can see all servers that were discovered
	assertTrue(topology.serverCount() >= 2);
}


void ReplicaSetTest::testTopologyMixedStandaloneAndPrimary()
{
	TopologyDescription topology;

	// Add a standalone server
	SocketAddress standaloneAddr("localhost:27017");
	topology.updateServer(standaloneAddr, *createStandaloneHelloResponse(), 1000);

	// Should be Single topology with one standalone
	assertEqual(static_cast<int>(TopologyDescription::Single), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add a replica set primary (incompatible!)
	SocketAddress primaryAddr("localhost:27018");
	topology.updateServer(primaryAddr, *createPrimaryHelloResponse(), 2000);

	// Mixed standalone + replica set primary should result in Unknown topology
	// Standalone cannot coexist with replica set members
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Server count should be >= 2 (the two servers we queried, plus any discovered hosts)
	// Note: Discovered hosts from primary's hello response are added even when types are incompatible
	// This preserves diagnostic information - we can see all servers that were discovered
	assertTrue(topology.serverCount() >= 2);
}


void ReplicaSetTest::testTopologyMultipleStandaloneWithSetName()
{
	// Test that multiple standalones with setName configured still results in Unknown
	// This is the critical bug fix from CodeQL analysis
	TopologyDescription topology("rs0"s);  // setName configured

	// Add first standalone server
	SocketAddress addr1("localhost:27017");
	topology.updateServer(addr1, *createStandaloneHelloResponse(), 1000);

	// Single standalone should be "Single" topology even with setName
	assertEqual(static_cast<int>(TopologyDescription::Single), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add second standalone server
	SocketAddress addr2("localhost:27018");
	topology.updateServer(addr2, *createStandaloneHelloResponse(), 2000);

	// Multiple standalone servers should result in Unknown topology
	// CRITICAL: Should be Unknown even though setName is configured
	// Previous buggy logic would return ReplicaSetNoPrimary due to !_setName.empty()
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertEqual(2, static_cast<int>(topology.serverCount()));
}


void ReplicaSetTest::testTopologyMixedMongosAndSecondary()
{
	TopologyDescription topology;

	// Add a mongos server
	SocketAddress mongosAddr("localhost:27017");
	topology.updateServer(mongosAddr, *createMongosHelloResponse(), 1000);

	// Should be Sharded topology with one mongos
	assertEqual(static_cast<int>(TopologyDescription::Sharded), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add a replica set secondary (incompatible!)
	SocketAddress secondaryAddr("localhost:27018");
	topology.updateServer(secondaryAddr, *createSecondaryHelloResponse(), 2000);

	// Mixed mongos + replica set secondary should result in Unknown topology
	// Cannot mix sharded and replica set topologies
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Server count should be >= 2 (the two servers we queried, plus any discovered hosts)
	// Note: Discovered hosts from secondary's hello response are added even when types are incompatible
	// This preserves diagnostic information - we can see all servers that were discovered
	assertTrue(topology.serverCount() >= 2);
}


void ReplicaSetTest::testTopologyMixedStandaloneAndSecondary()
{
	TopologyDescription topology;

	// Add a standalone server
	SocketAddress standaloneAddr("localhost:27017");
	topology.updateServer(standaloneAddr, *createStandaloneHelloResponse(), 1000);

	// Should be Single topology with one standalone
	assertEqual(static_cast<int>(TopologyDescription::Single), static_cast<int>(topology.type()));
	assertEqual(1, static_cast<int>(topology.serverCount()));

	// Add a replica set secondary (incompatible!)
	SocketAddress secondaryAddr("localhost:27018");
	topology.updateServer(secondaryAddr, *createSecondaryHelloResponse(), 2000);

	// Mixed standalone + replica set secondary should result in Unknown topology
	// Standalone cannot coexist with replica set members
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Server count should be >= 2 (the two servers we queried, plus any discovered hosts)
	// Note: Discovered hosts from secondary's hello response are added even when types are incompatible
	// This preserves diagnostic information - we can see all servers that were discovered
	assertTrue(topology.serverCount() >= 2);
}


void ReplicaSetTest::testTopologyTransitions()
{
	TopologyDescription topology;

	// Start with Unknown
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));

	// Add a secondary -> ReplicaSetNoPrimary
	SocketAddress secondary("localhost:27018");
	topology.updateServer(secondary, *createSecondaryHelloResponse(), 3000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());

	// Add a primary -> ReplicaSetWithPrimary
	SocketAddress primary("localhost:27017");
	topology.updateServer(primary, *createPrimaryHelloResponse(), 5000);

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetWithPrimary), static_cast<int>(topology.type()));
	assertTrue(topology.hasPrimary());

	// Mark primary as unknown -> back to ReplicaSetNoPrimary
	topology.markServerUnknown(primary, "Network error");

	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertFalse(topology.hasPrimary());

	// Remove all servers except one unknown -> Unknown
	topology.removeServer(secondary);
	// Now only the unknown primary remains
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));

	// Clear all -> Unknown
	topology.clear();
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertEqual(0, static_cast<int>(topology.serverCount()));
}


void ReplicaSetTest::testTopologyReplicaSetNoPrimaryWithSetName()
{
	// This test verifies that a topology with a configured setName
	// is classified as ReplicaSetNoPrimary (not Unknown) even with unknown servers
	TopologyDescription topology("rs0"s);

	// Topology has setName but no servers - should be Unknown (no servers at all)
	assertEqual(static_cast<int>(TopologyDescription::Unknown), static_cast<int>(topology.type()));
	assertEqual("rs0"s, topology.setName());

	// Add an unknown server
	SocketAddress addr("localhost:27017");
	topology.addServer(addr);

	// With setName configured and servers (even if all unknown),
	// topology should be ReplicaSetNoPrimary because we know it's intended to be a replica set
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));

	// Now add an arbiter (which is a replica set member)
	SocketAddress arbiter("localhost:27019");
	topology.updateServer(arbiter, *createArbiterHelloResponse(), 2000);

	// Should still be ReplicaSetNoPrimary (we have replica set members but no primary)
	assertEqual(static_cast<int>(TopologyDescription::ReplicaSetNoPrimary), static_cast<int>(topology.type()));
	assertFalse(topology.hasPrimary());
	assertTrue(topology.findSecondaries().empty());  // Arbiters are not secondaries
}


// ============================================================================
// ReadPreference Tests
// ============================================================================


void ReplicaSetTest::testReadPreferencePrimary()
{
	ReadPreference pref = ReadPreference::primary();

	assertEqual(static_cast<int>(ReadPreference::Primary), static_cast<int>(pref.mode()));
	assertEqual("primary"s, pref.toString());

	// Create topology with primary and secondaries
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	// Primary read preference should only select primary
	auto selected = pref.selectServers(topology);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(selected[0].type()));

	const SocketAddress& addr = selected[0].address();
	if (addr.family() == SocketAddress::IPv6)
	{
		assertEqual("[::1]:27017"s, selected[0].address().toString());
	}
	else
	{
		assertEqual("localhost:27017"s, selected[0].address().toString());
	}
}


void ReplicaSetTest::testReadPreferencePrimaryPreferred()
{
	ReadPreference pref = ReadPreference::primaryPreferred();

	assertEqual(static_cast<int>(ReadPreference::PrimaryPreferred), static_cast<int>(pref.mode()));

	// Create topology with primary and secondary
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	// Should select primary when available
	auto selected = pref.selectServers(topology);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(selected[0].type()));

	// Create topology with only secondary (no primary)
	TopologyDescription topologyNoPrimary;
	topologyNoPrimary.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	// Should fall back to secondary
	selected = pref.selectServers(topologyNoPrimary);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(selected[0].type()));
}


void ReplicaSetTest::testReadPreferenceSecondary()
{
	ReadPreference pref = ReadPreference::secondary();

	assertEqual(static_cast<int>(ReadPreference::Secondary), static_cast<int>(pref.mode()));

	// Create topology with primary and secondaries
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	// Should only select secondaries
	auto selected = pref.selectServers(topology);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(selected[0].type()));

	const SocketAddress& addr = selected[0].address();
	if (addr.family() == SocketAddress::IPv6)
	{
		assertEqual("[::1]:27018"s, selected[0].address().toString());
	}
	else
	{
		assertEqual("localhost:27018"s, selected[0].address().toString());
	}

	// Create topology with only primary (no secondaries)
	TopologyDescription topologyPrimaryOnly;
	topologyPrimaryOnly.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);

	// Should return empty (no secondaries available)
	selected = pref.selectServers(topologyPrimaryOnly);
	assertTrue(selected.empty());
}


void ReplicaSetTest::testReadPreferenceSecondaryPreferred()
{
	ReadPreference pref = ReadPreference::secondaryPreferred();

	assertEqual(static_cast<int>(ReadPreference::SecondaryPreferred), static_cast<int>(pref.mode()));

	// Create topology with primary and secondary
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	// Should select secondary when available
	auto selected = pref.selectServers(topology);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsSecondary), static_cast<int>(selected[0].type()));

	// Create topology with only primary (no secondaries)
	TopologyDescription topologyPrimaryOnly;
	topologyPrimaryOnly.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);

	// Should fall back to primary
	selected = pref.selectServers(topologyPrimaryOnly);
	assertEqual(1, static_cast<int>(selected.size()));
	assertEqual(static_cast<int>(ServerDescription::RsPrimary), static_cast<int>(selected[0].type()));
}


void ReplicaSetTest::testReadPreferenceNearest()
{
	ReadPreference pref = ReadPreference::nearest();

	assertEqual(static_cast<int>(ReadPreference::Nearest), static_cast<int>(pref.mode()));

	// Create topology with primary (5ms RTT) and secondaries (3ms and 8ms RTT)
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createSecondaryHelloResponse(), 3000);

	Document::Ptr secondary2Response = new Document();
	secondary2Response->add("isWritablePrimary"s, false);
	secondary2Response->add("secondary"s, true);
	secondary2Response->add("setName"s, "rs0"s);
	Array::Ptr hosts = new Array();
	hosts->add("localhost:27017"s);
	hosts->add("localhost:27018"s);
	hosts->add("localhost:27019"s);
	secondary2Response->add("hosts"s, hosts);
	secondary2Response->add("ok"s, 1.0);

	topology.updateServer(SocketAddress("localhost:27019"), *secondary2Response, 8000);

	// Nearest should select servers regardless of type (primary or secondary)
	// All servers should be eligible
	auto selected = pref.selectServers(topology);
	assertTrue(selected.size() >= 1);  // At least one server selected

	// The selection should include servers with lowest latency
	// In practice, nearest mode selects all servers within latency window
}


void ReplicaSetTest::testReadPreferenceWithTags()
{
	// Create tag set for targeting specific datacenter
	Document tags;
	tags.add("dc"s, "east"s);
	tags.add("rack"s, "1"s);

	ReadPreference pref(ReadPreference::Secondary, tags);

	assertEqual(static_cast<int>(ReadPreference::Secondary), static_cast<int>(pref.mode()));
	assertFalse(pref.tags().empty());
	assertEqual("east"s, pref.tags().get<std::string>("dc"s));

	// Create topology with tagged and untagged secondaries
	TopologyDescription topology;
	topology.updateServer(SocketAddress("localhost:27017"), *createPrimaryHelloResponse(), 5000);
	topology.updateServer(SocketAddress("localhost:27018"), *createTaggedSecondaryHelloResponse(), 3000);

	// Create untagged secondary
	Document::Ptr untaggedSecondary = new Document();
	untaggedSecondary->add("isWritablePrimary"s, false);
	untaggedSecondary->add("secondary"s, true);
	untaggedSecondary->add("setName"s, "rs0"s);
	Array::Ptr hosts = new Array();
	hosts->add("localhost:27017"s);
	hosts->add("localhost:27018"s);
	hosts->add("localhost:27019"s);
	untaggedSecondary->add("hosts"s, hosts);
	untaggedSecondary->add("ok"s, 1.0);

	topology.updateServer(SocketAddress("localhost:27019"), *untaggedSecondary, 4000);

	// Should select only tagged secondary matching the tag set
	auto selected = pref.selectServers(topology);

	// At least the tagged server should be selected
	[[maybe_unused]] bool foundTaggedServer = false;
	for (const auto& server : selected)
	{
		if (server.address().toString() == "localhost:27018"s)
		{
			foundTaggedServer = true;
			assertEqual("east"s, server.tags().get<std::string>("dc"s));
			assertEqual("1"s, server.tags().get<std::string>("rack"s));
		}
	}

	// The tagged server may or may not be selected depending on implementation
	// This test verifies the tag data is preserved correctly
}


// ============================================================================
// ReplicaSet URI Parsing Tests
// ============================================================================


void ReplicaSetTest::testReplicaSetURIParsing()
{
	// This test verifies that MongoDB replica set URIs with multiple comma-separated
	// hosts are parsed correctly. The URI parsing should not throw a SyntaxException
	// even though it will fail to connect (since there are no real servers).

	// Test case 1: URI with multiple hosts and query parameters
	std::string uri1 = "mongodb://localhost:27017,host1:27017,host2:27017/?reconnectRetries=10&reconnectDelay=2";

	try
	{
		// This will throw an exception because there are no real servers to connect to,
		// but it should NOT throw a SyntaxException if URI parsing works correctly
		ReplicaSet rs(uri1);

		// If we get here, the URI was parsed successfully and servers were reachable
		// (which shouldn't happen in unit tests, but we handle it gracefully)
		assertTrue(true);
	}
	catch (const Poco::SyntaxException& e)
	{
		// This should NOT happen - if we get a SyntaxException, the URI parsing failed
		std::string msg = "URI parsing failed with SyntaxException: " + e.displayText();
		fail(msg);
	}
	catch (const Poco::UnknownURISchemeException& e)
	{
		// This should NOT happen - the scheme is correct
		std::string msg = "URI parsing failed with UnknownURISchemeException: " + e.displayText();
		fail(msg);
	}
	catch (...)
	{
		// Any other exception (e.g., connection failure) is expected and acceptable
		// We only care that URI parsing succeeded
		assertTrue(true);
	}

	// Test case 2: URI with three hosts and replica set name
	std::string uri2 = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0";

	try
	{
		ReplicaSet rs(uri2);
		assertTrue(true);
	}
	catch (const Poco::SyntaxException& e)
	{
		std::string msg = "URI parsing failed for uri2 with SyntaxException: " + e.displayText();
		fail(msg);
	}
	catch (const Poco::UnknownURISchemeException& e)
	{
		std::string msg = "URI parsing failed for uri2 with UnknownURISchemeException: " + e.displayText();
		fail(msg);
	}
	catch (...)
	{
		// Connection failure is expected
		assertTrue(true);
	}

	// Test case 3: URI with multiple localhost servers
	std::string uri3 = "mongodb://127.0.0.1:27017,127.0.0.1:27018/?readPreference=primaryPreferred";

	try
	{
		ReplicaSet rs(uri3);
		assertTrue(true);
	}
	catch (const Poco::SyntaxException& e)
	{
		std::string msg = "URI parsing failed for uri3 with SyntaxException: " + e.displayText();
		fail(msg);
	}
	catch (const Poco::UnknownURISchemeException& e)
	{
		std::string msg = "URI parsing failed for uri3 with UnknownURISchemeException: " + e.displayText();
		fail(msg);
	}
	catch (...)
	{
		// Connection failure is expected
		assertTrue(true);
	}
}


// ============================================================================
// ReplicaSetURI Class Tests
// ============================================================================


void ReplicaSetTest::testReplicaSetURIClass()
{
	// Test parsing a complex URI
	std::string uri = "mongodb://user:pass@localhost:27017,localhost:27018,localhost:27019/testdb?replicaSet=rs0&readPreference=secondaryPreferred&connectTimeoutMS=5000&socketTimeoutMS=15000&heartbeatFrequencyMS=20000&reconnectRetries=5&reconnectDelay=3";

	ReplicaSetURI parsedURI(uri);

	// Verify servers (stored as strings, not resolved)
	const auto& servers = parsedURI.servers();
	assertEqual(3, static_cast<int>(servers.size()));
	assertEqual("localhost:27017"s, servers[0]);
	assertEqual("localhost:27018"s, servers[1]);
	assertEqual("localhost:27019"s, servers[2]);

	// Verify configuration
	assertEqual("rs0"s, parsedURI.replicaSet());
	assertEqual(static_cast<int>(ReadPreference::SecondaryPreferred), static_cast<int>(parsedURI.readPreference().mode()));
	assertEqual(5000u, parsedURI.connectTimeoutMS());
	assertEqual(15000u, parsedURI.socketTimeoutMS());
	assertEqual(20000u, parsedURI.heartbeatFrequencyMS());
	assertEqual(5u, parsedURI.reconnectRetries());
	assertEqual(3u, parsedURI.reconnectDelay());

	// Verify database and user info
	assertEqual("testdb"s, parsedURI.database());
	assertEqual("user"s, parsedURI.username());
	assertEqual("pass"s, parsedURI.password());

	// Test parsing URI without optional parameters
	std::string simpleUri = "mongodb://localhost:27017,localhost:27018";
	ReplicaSetURI simpleURI(simpleUri);

	const auto& simpleServers = simpleURI.servers();
	assertEqual(2, static_cast<int>(simpleServers.size()));
	assertTrue(simpleURI.replicaSet().empty());
	assertTrue(simpleURI.database().empty());
	assertTrue(simpleURI.username().empty());
	assertEqual(static_cast<int>(ReadPreference::Primary), static_cast<int>(simpleURI.readPreference().mode()));
}


void ReplicaSetTest::testReplicaSetURIToString()
{
	// Create a URI object and set properties
	ReplicaSetURI uri;

	uri.addServer("localhost:27017"s);
	uri.addServer("localhost:27018"s);
	uri.addServer("localhost:27019"s);
	uri.setReplicaSet("rs0"s);
	uri.setReadPreference("secondary"s);
	uri.setConnectTimeoutMS(5000);
	uri.setReconnectRetries(5);
	uri.setReconnectDelay(2);

	std::string uriString = uri.toString();

	// Parse the generated URI back
	ReplicaSetURI parsedBack(uriString);

	// Verify round-trip
	assertEqual(3, static_cast<int>(parsedBack.servers().size()));
	assertEqual("rs0"s, parsedBack.replicaSet());
	assertEqual(static_cast<int>(ReadPreference::Secondary), static_cast<int>(parsedBack.readPreference().mode()));
	assertEqual(5000u, parsedBack.connectTimeoutMS());
	assertEqual(5u, parsedBack.reconnectRetries());
	assertEqual(2u, parsedBack.reconnectDelay());

	// Test URI with database and user info
	ReplicaSetURI uriWithAuth;
	uriWithAuth.addServer("localhost:27017"s);
	uriWithAuth.setUsername("admin"s);
	uriWithAuth.setPassword("secret"s);
	uriWithAuth.setDatabase("mydb"s);

	std::string authUriString = uriWithAuth.toString();

	// Verify the URI contains the expected components
	assertTrue(authUriString.find("admin:secret@") != std::string::npos);
	assertTrue(authUriString.find("localhost:27017") != std::string::npos);
	assertTrue(authUriString.find("/mydb") != std::string::npos);
}


void ReplicaSetTest::testReplicaSetURIModification()
{
	// Start with a parsed URI
	std::string originalUri = "mongodb://localhost:27017,localhost:27018/?replicaSet=rs0&reconnectRetries=10";
	ReplicaSetURI uri(originalUri);

	// Verify initial state
	assertEqual(2, static_cast<int>(uri.servers().size()));
	assertEqual("rs0"s, uri.replicaSet());
	assertEqual(10u, uri.reconnectRetries());

	// Modify servers
	uri.addServer("localhost:27019"s);
	assertEqual(3, static_cast<int>(uri.servers().size()));

	// Modify configuration
	uri.setReplicaSet("rs1"s);
	uri.setReadPreference("primaryPreferred"s);
	uri.setReconnectRetries(20);

	assertEqual("rs1"s, uri.replicaSet());
	assertEqual(static_cast<int>(ReadPreference::PrimaryPreferred), static_cast<int>(uri.readPreference().mode()));
	assertEqual(20u, uri.reconnectRetries());

	// Clear and reset servers
	uri.clearServers();
	assertEqual(0, static_cast<int>(uri.servers().size()));

	uri.addServer("127.0.0.1:27017"s);
	assertEqual(1, static_cast<int>(uri.servers().size()));
	assertEqual("127.0.0.1:27017"s, uri.servers()[0]);

	// Test setServers
	std::vector<std::string> newServers;
	newServers.push_back("host1:27020"s);
	newServers.push_back("host2:27021"s);
	uri.setServers(newServers);

	assertEqual(2, static_cast<int>(uri.servers().size()));
	assertEqual("host1:27020"s, uri.servers()[0]);
	assertEqual("host2:27021"s, uri.servers()[1]);

	// Verify the modified URI can be converted to string
	std::string modifiedUri = uri.toString();
	assertTrue(modifiedUri.find("host1:27020") != std::string::npos);
	assertTrue(modifiedUri.find("host2:27021") != std::string::npos);
}


void ReplicaSetTest::testReplicaSetWithURIObject()
{
	// Test creating a ReplicaSet using a ReplicaSetURI object
	// This allows programmatic configuration before connecting

	ReplicaSetURI uri;
	uri.addServer("localhost:27017"s);
	uri.addServer("localhost:27018"s);
	uri.setReplicaSet("rs0"s);
	uri.setReadPreference("secondaryPreferred"s);
	uri.setConnectTimeoutMS(5000);
	uri.setReconnectRetries(5);
	uri.setReconnectDelay(2);

	try
	{
		// Create ReplicaSet from the URI object
		ReplicaSet rs(uri);

		// Verify configuration was applied
		ReplicaSet::Config config = rs.configuration();

		assertEqual(2, static_cast<int>(config.seeds.size()));
		assertEqual("rs0"s, config.setName);
		assertEqual(static_cast<int>(ReadPreference::SecondaryPreferred), static_cast<int>(config.readPreference.mode()));
		assertEqual(5u, static_cast<unsigned int>(config.connectTimeoutSeconds));  // 5000ms -> 5s
		assertEqual(5u, static_cast<unsigned int>(config.serverReconnectRetries));
		assertEqual(2u, config.serverReconnectDelaySeconds);

		// If we get here, URI object constructor worked
		assertTrue(true);
	}
	catch (const Poco::SyntaxException& e)
	{
		std::string msg = "ReplicaSet construction with URI object failed with SyntaxException: " + e.displayText();
		fail(msg);
	}
	catch (const Poco::UnknownURISchemeException& e)
	{
		std::string msg = "ReplicaSet construction with URI object failed with UnknownURISchemeException: " + e.displayText();
		fail(msg);
	}
	catch (...)
	{
		// Connection failure is expected since servers don't exist
		// We only care that the configuration was properly extracted from the URI object
		assertTrue(true);
	}

	// Test that empty URI object throws exception
	ReplicaSetURI emptyUri;

	bool exceptionThrown = false;
	try
	{
		ReplicaSet rs(emptyUri);
	}
	catch (const Poco::InvalidArgumentException& e)
	{
		// Expected - URI must contain at least one host
		exceptionThrown = true;
		assertTrue(e.displayText().find("at least one") != std::string::npos);
	}
	catch (...)
	{
		// Any other exception is also acceptable for empty URI
		exceptionThrown = true;
	}

	assertTrue(exceptionThrown);
}


// ============================================================================
// Timeout and Non-Hang Tests
// ============================================================================


void ReplicaSetTest::testConnectionTimeoutWithUnreachableServer()
{
	// Use TEST-NET-1 (RFC 5737) — a non-routable IP that will always time out
	// This verifies that Connection::connect() with a timeout does not hang
	SocketAddress unreachable("192.0.2.1:27017");

	Connection conn;
	Poco::Timespan connectTimeout(2, 0);  // 2 seconds
	Poco::Timespan socketTimeout(2, 0);

	auto start = std::chrono::steady_clock::now();

	bool exceptionThrown = false;
	try
	{
		conn.connect(unreachable, connectTimeout, socketTimeout);
	}
	catch (const Poco::Exception&)
	{
		exceptionThrown = true;
	}
	catch (...)
	{
		exceptionThrown = true;
	}

	auto elapsed = std::chrono::steady_clock::now() - start;
	auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();

	assertTrue(exceptionThrown);
	// Must complete within a reasonable margin above the timeout (2s timeout + some margin)
	// If the timeout is not working, this would hang for minutes
	assertTrue(elapsedSeconds < 10);
}


void ReplicaSetTest::testReplicaSetConstructionTimeout()
{
	// Verify that ReplicaSet construction with unreachable servers does not hang.
	// Uses non-routable IPs with short timeouts and non-zero reconnect retries
	// to exercise the retry path during initial topology discovery.
	ReplicaSet::Config config;
	config.seeds = {
		SocketAddress("192.0.2.1:27017"),
		SocketAddress("192.0.2.2:27017")
	};
	config.setName = "rs0";
	config.connectTimeoutSeconds = 1;
	config.socketTimeoutSeconds = 1;
	config.heartbeatFrequencySeconds = 10;
	config.serverReconnectRetries = 2;
	config.serverReconnectDelaySeconds = 1;
	config.enableMonitoring = false;  // Don't start monitor thread

	auto start = std::chrono::steady_clock::now();

	// Construction calls updateTopologyFromAllServers() which connects to each
	// seed sequentially. With 2 unreachable seeds × 1s connect timeout = ~2s
	// for the initial pass. serverReconnectRetries is not used during construction
	// itself, but is configured for later getConnection/waitForServerAvailability.
	ReplicaSet rs(config);

	auto elapsed = std::chrono::steady_clock::now() - start;
	auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();

	// 2 seeds × 1s timeout = ~2s expected; allow margin
	assertTrue(elapsedSeconds < 15);

	// Topology should show all servers as unknown/errored
	auto topology = rs.topology();
	assertFalse(topology.hasPrimary());

	// No connection should be available
	Connection::Ptr conn = rs.getPrimaryConnection();
	assertTrue(conn.isNull());
}


void ReplicaSetTest::testReplicaSetMonitoringStopWithUnreachableServers()
{
	// Verify that stopping a ReplicaSet with monitoring enabled and unreachable servers
	// does not hang — the destructor must return promptly.
	// Non-zero retries verify that the monitor thread's heartbeat cycle (which may
	// internally retry connections) does not prevent a clean shutdown.
	ReplicaSet::Config config;
	config.seeds = {
		SocketAddress("192.0.2.1:27017")
	};
	config.setName = "rs0";
	config.connectTimeoutSeconds = 1;
	config.socketTimeoutSeconds = 1;
	config.heartbeatFrequencySeconds = 60;  // Long heartbeat so monitor is likely sleeping
	config.serverReconnectRetries = 3;
	config.serverReconnectDelaySeconds = 1;
	config.enableMonitoring = true;  // Enable monitoring thread

	auto start = std::chrono::steady_clock::now();

	{
		ReplicaSet rs(config);
		// At this point, the monitor thread is running.
		// The destructor (~ReplicaSet) must stop the monitor thread promptly,
		// even though serverReconnectRetries > 0.
	}
	// ReplicaSet is destroyed here

	auto elapsed = std::chrono::steady_clock::now() - start;
	auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();

	// Construction (1 seed × 1s timeout = ~1s) + destruction (monitor stop)
	// should complete well within 10 seconds. If stopMonitoring() or the
	// destructor hangs (e.g. stuck in a reconnect-retry loop), this will fail.
	assertTrue(elapsedSeconds < 10);
}


void ReplicaSetTest::testConnectionPoolTimeoutWithUnreachableServers()
{
	// Verify that PooledReplicaSetConnection with timeouts set on the factory
	// does not hang when all servers are unreachable.
	//
	// The factory passes connectTimeout/socketTimeout to ReplicaSetConnection,
	// which uses them in ensureConnection() → getConnection(readPref, timeouts).
	// On failure, executeWithRetry() → waitForServerAvailability() loops through
	// serverReconnectRetries from the ReplicaSet config.
	//
	// Expected worst-case time:
	//   construction:  2 seeds × 1s = ~2s
	//   sendRequest:   retries × (delay + seeds × connectTimeout)
	//                  = 2 × (1 + 2 × 1) = ~6s
	//   total:         ~8s
	ReplicaSet::Config config;
	config.seeds = {
		SocketAddress("192.0.2.1:27017"),
		SocketAddress("192.0.2.2:27017")
	};
	config.setName = "rs0";
	config.connectTimeoutSeconds = 1;
	config.socketTimeoutSeconds = 1;
	config.heartbeatFrequencySeconds = 60;
	config.serverReconnectRetries = 2;
	config.serverReconnectDelaySeconds = 1;
	config.enableMonitoring = false;

	auto start = std::chrono::steady_clock::now();

	ReplicaSet rs(config);

	// Create pool factory with explicit timeouts — these are passed to
	// ReplicaSetConnection and used for each connection attempt
	ReadPreference readPref = ReadPreference::primary();
	Poco::Timespan connectTimeout(1, 0);
	Poco::Timespan socketTimeout(1, 0);
	Poco::PoolableObjectFactory<ReplicaSetConnection, ReplicaSetConnection::Ptr> factory(
		rs, readPref, connectTimeout, socketTimeout);
	Poco::ObjectPool<ReplicaSetConnection, ReplicaSetConnection::Ptr> pool(factory, 2, 5);

	// Use PooledReplicaSetConnection (RAII) — borrow is automatic, return on scope exit
	bool exceptionThrown = false;
	try
	{
		PooledReplicaSetConnection conn(pool);

		OpMsgMessage request("admin"s, ""s);
		request.setCommandName(OpMsgMessage::CMD_HELLO);
		OpMsgMessage response;
		conn->sendRequest(request, response);
	}
	catch (const Poco::Exception&)
	{
		exceptionThrown = true;
	}
	catch (...)
	{
		exceptionThrown = true;
	}

	assertTrue(exceptionThrown);

	auto elapsed = std::chrono::steady_clock::now() - start;
	auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();

	// With 2 retries × (1s delay + 2 seeds × 1s timeout) = ~6s for the retry loop,
	// plus ~2s for construction. Total ~8s expected. Allow generous margin for
	// CI variability, but it must not hang (which would be minutes/infinite).
	assertTrue(elapsedSeconds < 60);
}


void ReplicaSetTest::testPlainConnectionPoolTimeoutWithUnreachableServer()
{
	// Verify that PooledConnection with the timeout-aware factory
	// does not hang when the server is unreachable.
	SocketAddress unreachable("192.0.2.1:27017");
	Poco::Timespan connectTimeout(2, 0);  // 2 seconds
	Poco::Timespan socketTimeout(2, 0);

	Poco::PoolableObjectFactory<Connection, Connection::Ptr> factory(
		unreachable, connectTimeout, socketTimeout);
	Poco::ObjectPool<Connection, Connection::Ptr> pool(factory, 2, 5);

	auto start = std::chrono::steady_clock::now();

	// Use PooledConnection (RAII) — borrowObject is called in the constructor.
	// With an unreachable server, createObject() will throw after ~2 seconds.
	bool exceptionThrown = false;
	try
	{
		PooledConnection conn(pool);
	}
	catch (const Poco::Exception&)
	{
		exceptionThrown = true;
	}
	catch (...)
	{
		exceptionThrown = true;
	}

	auto elapsed = std::chrono::steady_clock::now() - start;
	auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();

	assertTrue(exceptionThrown);
	// Must complete within a reasonable margin above the 2s timeout.
	// Without the timeout, this would hang for minutes (OS default TCP timeout).
	assertTrue(elapsedSeconds < 10);
}


CppUnit::Test* ReplicaSetTest::suite()
{
	auto* pSuite = new CppUnit::TestSuite("ReplicaSetTest");

	// ServerDescription tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionSecondary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionArbiter);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionStandalone);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionMongos);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionWithTags);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionWithHosts);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionErrorHandling);
	CppUnit_addTest(pSuite, ReplicaSetTest, testServerDescriptionReset);

	// TopologyDescription tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyEmpty);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyAddServers);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyUpdateToPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyUpdateToSecondary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyReplicaSetWithPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyReplicaSetNoPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyStandalone);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologySharded);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyFindPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyFindSecondaries);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMarkServerUnknown);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyRemoveServer);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyDiscoverNewHosts);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologySetNameMismatch);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMixedUnknownAndKnown);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyAllUnknown);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMultipleStandalone);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMixedMongosAndPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMixedStandaloneAndPrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMultipleStandaloneWithSetName);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMixedMongosAndSecondary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyMixedStandaloneAndSecondary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyTransitions);
	CppUnit_addTest(pSuite, ReplicaSetTest, testTopologyReplicaSetNoPrimaryWithSetName);

	// ReadPreference tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferencePrimary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferencePrimaryPreferred);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferenceSecondary);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferenceSecondaryPreferred);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferenceNearest);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReadPreferenceWithTags);

	// ReplicaSet URI parsing tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIParsing);

	// ReplicaSetURI class tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIClass);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIToString);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIModification);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetWithURIObject);

	// Timeout and non-hang tests
	CppUnit_addTest(pSuite, ReplicaSetTest, testConnectionTimeoutWithUnreachableServer);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetConstructionTimeout);
	CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetMonitoringStopWithUnreachableServers);
	CppUnit_addTest(pSuite, ReplicaSetTest, testConnectionPoolTimeoutWithUnreachableServers);
	CppUnit_addTest(pSuite, ReplicaSetTest, testPlainConnectionPoolTimeoutWithUnreachableServer);

	return pSuite;
}
