#include <TestSupport.h>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/thread.hpp>
#include <oxt/system_calls.hpp>
#include <vector>
#include <BackgroundEventLoop.h>
#include <ServerKit/Server.h>
#include <ServerKit/ClientRef.h>
#include <LoggingKit/LoggingKit.h>
#include <FileDescriptor.h>
#include <IOTools/IOUtils.h>

using namespace Passenger;
using namespace Passenger::ServerKit;
using namespace Passenger::MemoryKit;
using namespace std;
using namespace oxt;

namespace tut {
	struct ServerKit_ServerTest: public TestBase {
		typedef ClientRef<Server<Client>, Client> ClientRefType;

		BackgroundEventLoop bg;
		Json::Value config;
		ServerKit::Schema skSchema;
		ServerKit::Context context;
		ServerKit::BaseServerSchema schema;
		boost::shared_ptr< Server<Client> > server;
		int serverSocket1, serverSocket2;

		ServerKit_ServerTest()
			: bg(false, true),
			  context(skSchema)
		{
			if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) {
				// If the user did not customize the test's log level,
				// then we'll want to tone down the noise.
				LoggingKit::setLevel(LoggingKit::CRIT);
			}
			context.libev = bg.safe;
			context.libuv = bg.libuv_loop;
			context.initialize();
			serverSocket1 = createUnixServer("tmp.server1");
			serverSocket2 = createUnixServer("tmp.server2");
		}

		~ServerKit_ServerTest() {
			if (!bg.isStarted()) {
				bg.start();
			}
			if (server != NULL) {
				bg.safe->runSync(boost::bind(&Server<Client>::shutdown, server.get(), true));
				while (getServerState() != Server<Client>::FINISHED_SHUTDOWN) {
					syscalls::usleep(10000);
				}
			}
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::destroyServer,
				this));
			safelyClose(serverSocket1);
			safelyClose(serverSocket2);
			unlink("tmp.server1");
			unlink("tmp.server2");
			bg.stop();
		}

		void init() {
			server = boost::make_shared< Server<Client> >(&context, schema, config);
			server->initialize();
			server->listen(serverSocket1);
		}

		template<typename ServerClass>
		void initWithServerClass() {
			server = boost::make_shared<ServerClass>(&context, schema, config);
			server->initialize();
			server->listen(serverSocket1);
		}

		void startServer() {
			bg.start();
		}

		void destroyServer() {
			server.reset();
		}

		FileDescriptor connectToServer1() {
			return FileDescriptor(connectToUnixServer("tmp.server1", __FILE__, __LINE__), NULL, 0);
		}

		FileDescriptor connectToServer2() {
			return FileDescriptor(connectToUnixServer("tmp.server2", __FILE__, __LINE__), NULL, 0);
		}

		Server<Client>::State getServerState() {
			Server<Client>::State result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_getServerState,
				this, &result));
			return result;
		}

		void _getServerState(Server<Client>::State *state) {
			*state = server->serverState;
		}

		unsigned int getActiveClientCount() {
			int result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_getActiveClientCount,
				this, &result));
			return result;
		}

		void _getActiveClientCount(int *result) {
			*result = server->activeClientCount;
		}

		unsigned int getDisconnectedClientCount() {
			int result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_getDisconnectedClientCount,
				this, &result));
			return result;
		}

		void _getDisconnectedClientCount(int *result) {
			*result = server->disconnectedClientCount;
		}

		unsigned int getFreeClientCount() {
			int result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_getFreeClientCount,
				this, &result));
			return result;
		}

		void _getFreeClientCount(int *result) {
			*result = server->freeClientCount;
		}

		vector<ClientRefType> getActiveClients() {
			vector<ClientRefType> result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_getActiveClients,
				this, &result));
			return result;
		}

		void _getActiveClients(vector<ClientRefType> *result) {
			*result = server->getActiveClients();
		}

		bool clientIsConnected(Client *client) {
			bool result;
			bg.safe->runSync(boost::bind(&ServerKit_ServerTest::_clientIsConnected,
				this, client, &result));
			return result;
		}

		void _clientIsConnected(Client *client, bool *result) {
			*result = client->connected();
		}
	};

	DEFINE_TEST_GROUP(ServerKit_ServerTest);


	/***** Initial state *****/

	TEST_METHOD(1) {
		set_test_name("It has no clients at startup");
		init();
		ensure_equals(server->activeClientCount, 0u);
		ensure_equals(server->disconnectedClientCount, 0u);
		ensure_equals(server->freeClientCount, 0u);
	}


	/***** Client object management *****/

	TEST_METHOD(5) {
		set_test_name("Accepting a new client works");

		init();
		startServer();
		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);
	}

	TEST_METHOD(6) {
		set_test_name("When a client is accepted, and the freelist is non-empty, "
			"the object is checked out from the freelist");

		config["min_spare_clients"] = 1;
		init();
		server->createSpareClients();
		startServer();

		ensure_equals(getActiveClientCount(), 0u);
		ensure_equals(getDisconnectedClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 1u);

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);
		ensure_equals(getDisconnectedClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 0u);
	}

	TEST_METHOD(7) {
		set_test_name("When a client is accepted, and the freelist is empty, "
			"the object is allocated");

		init();
		startServer();
		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);
		ensure_equals(getDisconnectedClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 0u);
	}

	TEST_METHOD(8) {
		set_test_name("Once a client has been disconnected, and the freelist has not "
			"yet reached the limit, the client object is put on the freelist");

		config["client_freelist_limit"] = 10;
		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);
		fd.close();
		EVENTUALLY(5,
			result = getActiveClientCount() == 0u;
		);
		ensure_equals(getDisconnectedClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 1u);
	}

	TEST_METHOD(9) {
		set_test_name("Once a client has been disconnected, and the freelist has "
			"reached the limit, the client object is destroyed");

		config["client_freelist_limit"] = 0;
		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);
		fd.close();
		EVENTUALLY(5,
			result = getActiveClientCount() == 0u;
		);
		ensure_equals(getDisconnectedClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 0u);
	}

	TEST_METHOD(10) {
		set_test_name("Once a client has been disconnected, the freelist has not yet reached "
			"the limit, and there are still references to the client, then the client is first "
			"put in the disconnecting list, then in the freelist when the last references disappear");

		vector<ClientRefType> clients;
		config["client_freelist_limit"] = 10;
		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);

		clients = getActiveClients();
		fd.close();
		EVENTUALLY(5,
			result = getActiveClientCount() == 0u;
		);
		ensure_equals(getDisconnectedClientCount(), 1u);
		ensure_equals(getFreeClientCount(), 0u);

		clients.clear();
		EVENTUALLY(5,
			result = getDisconnectedClientCount() == 0u;
		);
		ensure_equals(getActiveClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 1u);
	}

	TEST_METHOD(11) {
		set_test_name("Once a client has been disconnected, the freelist has reached the limit, "
			"and there are still references to the client, then the client is first put in the "
			"disconnecting list, then destroyed when the last references disappear");

		vector<ClientRefType> clients;
		config["client_freelist_limit"] = 0;
		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);

		clients = getActiveClients();
		fd.close();
		EVENTUALLY(5,
			result = getActiveClientCount() == 0u;
		);
		ensure_equals(getDisconnectedClientCount(), 1u);
		ensure_equals(getFreeClientCount(), 0u);

		clients.clear();
		EVENTUALLY(5,
			result = getDisconnectedClientCount() == 0u;
		);
		ensure_equals(getActiveClientCount(), 0u);
		ensure_equals(getFreeClientCount(), 0u);
	}


	/****** Multiple listen endpoints *****/

	TEST_METHOD(20) {
		set_test_name("It can listen on multiple endpoints");

		init();
		server->listen(serverSocket2);
		startServer();

		FileDescriptor fd1 = connectToServer1();
		FileDescriptor fd2 = connectToServer2();
		EVENTUALLY(5,
			result = getActiveClientCount() == 2u;
		);
	}


	/****** Input and output *****/

	class Test25Server: public Server<Client> {
	protected:
		virtual Channel::Result onClientDataReceived(Client *client,
			const MemoryKit::mbuf &buffer, int errcode)
		{
			if (errcode != 0 || buffer.empty()) {
				disconnect(&client);
			} else {
				boost::lock_guard<boost::mutex> l(syncher);
				data.append(buffer.start, buffer.size());
			}
			return Channel::Result(buffer.size(), false);
		}

	public:
		boost::mutex syncher;
		string data;

		Test25Server(Context *ctx, const ServerKit::BaseServerSchema &schema,
			const Json::Value &initialConfig)
			: Server<Client>(ctx, schema, initialConfig)
			{ }
	};

	TEST_METHOD(25) {
		set_test_name("Input is made available through client->input");

		initWithServerClass<Test25Server>();
		startServer();

		FileDescriptor fd(connectToServer1());
		writeExact(fd, "hello", 5);

		EVENTUALLY(5,
			Test25Server *s = (Test25Server *) server.get();
			boost::lock_guard<boost::mutex> l(s->syncher);
			result = s->data == "hello";
		);
	}

	class Test26Server: public Server<Client> {
	protected:
		virtual Channel::Result onClientDataReceived(Client *client,
			const MemoryKit::mbuf &buffer, int errcode)
		{
			if (errcode != 0 || buffer.empty()) {
				disconnect(&client);
			} else {
				client->output.feed(buffer);
			}
			return Channel::Result(buffer.size(), false);
		}

	public:
		Test26Server(Context *ctx, const ServerKit::BaseServerSchema &schema,
			const Json::Value &initialConfig)
			: Server<Client>(ctx, schema, initialConfig)
			{ }
	};

	TEST_METHOD(26) {
		set_test_name("Output is made available through client->output");

		initWithServerClass<Test26Server>();
		startServer();

		FileDescriptor fd(connectToServer1());
		writeExact(fd, "hello", 5);
		syscalls::shutdown(fd, SHUT_WR);
		ensure_equals(readAll(fd, 1024).first, "hello");
	}

	TEST_METHOD(27) {
		set_test_name("The file descriptor can be obtained through client->getFd()");

		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);

		ClientRefType client = getActiveClients()[0];
		writeExact(client.get()->getFd(), "hello", 5);

		char buf[5];
		readExact(fd, buf, 5);
		ensure_equals(StaticString(buf, 5), StaticString("hello"));
	}

	TEST_METHOD(28) {
		set_test_name("When a client is disconnected, client->connected() becomes false");

		init();
		startServer();

		FileDescriptor fd(connectToServer1());
		EVENTUALLY(5,
			result = getActiveClientCount() == 1u;
		);

		ClientRefType client = getActiveClients()[0];
		ensure(clientIsConnected(client.get()));
		fd.close();
		EVENTUALLY(5,
			result = !clientIsConnected(client.get());
		);
	}
}
