#include "TestSupport.h"

#include <boost/thread.hpp>
#include <boost/bind.hpp>

#include "MessageServer.h"
#include "ApplicationPool/Client.h"
#include "Utils.h"
#include <string>
#include <cstring>
#include <unistd.h>
#include <errno.h>

using namespace Passenger;
using namespace Passenger::ApplicationPool;
using namespace boost;
using namespace std;

namespace tut {
	struct MessageServerTest {
		ServerInstanceDirPtr serverInstanceDir;
		ServerInstanceDir::GenerationPtr generation;
		string socketFilename;
		AccountsDatabasePtr accountsDatabase;
		AccountPtr clientAccount;
		shared_ptr<MessageServer> server;
		shared_ptr<oxt::thread> serverThread;
		
		MessageServerTest() {
			createServerInstanceDirAndGeneration(serverInstanceDir, generation);
			socketFilename = generation->getPath() + "/socket";
			accountsDatabase = ptr(new AccountsDatabase());
			clientAccount = accountsDatabase->add("test", "12345", false);
			
			server = ptr(new MessageServer(socketFilename, accountsDatabase));
			serverThread  = ptr(new oxt::thread(
				boost::bind(&MessageServer::mainLoop, server.get())
			));
		}
		
		~MessageServerTest() {
			if (serverThread != NULL) {
				serverThread->interrupt_and_join();
			}
			Passenger::setLogLevel(0);
		}
		
		class SlowClient: public Client {
		private:
			unsigned int timeToSendUsername;
			unsigned int timeToSendPassword;
			
		protected:
			virtual void sendUsername(MessageChannel &channel, const string &username) {
				if (timeToSendUsername > 0) {
					usleep(timeToSendUsername * 1000);
				}
				channel.writeScalar(username);
			}

			virtual void sendPassword(MessageChannel &channel, const StaticString &userSuppliedPassword) {
				if (timeToSendPassword > 0) {
					usleep(timeToSendPassword * 1000);
				}
				channel.writeScalar(userSuppliedPassword.c_str(), userSuppliedPassword.size());
			}
			
		public:
			SlowClient(unsigned int timeToSendUsername,
			           unsigned int timeToSendPassword)
			         : Client()
			{
				this->timeToSendUsername = timeToSendUsername;
				this->timeToSendPassword = timeToSendPassword;
			}
		};
		
		class CustomClient: public Client {
		public:
			CustomClient *sendText(const string &text) {
				data->channel.write(text.c_str(), NULL);
				return this;
			}
		};
		
		class LoggingHandler: public MessageServer::Handler {
		public:
			struct SpecificContext: public MessageServer::ClientContext {
				int id;
				
				SpecificContext(int i) {
					id = i;
				}
			};
			
			typedef shared_ptr<SpecificContext> SpecificContextPtr;
			
			boost::mutex mutex;
			volatile int clientsAccepted;
			volatile int clientsDisconnected;
			string receivedData;
			volatile int id;
			volatile int returnValue;
			SpecificContextPtr latestContext;
			
			LoggingHandler() {
				clientsAccepted = 0;
				clientsDisconnected = 0;
				returnValue = true;
			}
			
			virtual MessageServer::ClientContextPtr newClient(MessageServer::CommonClientContext &context) {
				boost::lock_guard<boost::mutex> l(mutex);
				clientsAccepted++;
				return ptr(new SpecificContext(id));
			}
			
			virtual void clientDisconnected(MessageServer::CommonClientContext &context,
			                                MessageServer::ClientContextPtr &handlerSpecificContext)
			{
				boost::lock_guard<boost::mutex> l(mutex);
				clientsDisconnected++;
			}
			
			virtual bool processMessage(MessageServer::CommonClientContext &commonContext,
			                            MessageServer::ClientContextPtr &handlerSpecificContext,
			                            const vector<string> &args)
			{
				boost::lock_guard<boost::mutex> l(mutex);
				latestContext = static_pointer_cast<SpecificContext>(handlerSpecificContext);
				receivedData.append(args[0]);
				return returnValue;
			}
		};
		
		typedef shared_ptr<LoggingHandler> LoggingHandlerPtr;
		
		class ProcessMessageReturnsFalseHandler: public MessageServer::Handler {
		public:
			virtual bool processMessage(MessageServer::CommonClientContext &commonContext,
			                            MessageServer::ClientContextPtr &handlerSpecificContext,
			                            const vector<string> &args)
			{
				return false;
			}
		};
	};

	DEFINE_TEST_GROUP(MessageServerTest);
	
	TEST_METHOD(1) {
		// It rejects the connection if the an invalid username or password was sent.
		accountsDatabase->add("hashed_user", Account::createHash("67890"), true);
		
		try {
			Client().connect(socketFilename, "testt", "12345");
			fail("SecurityException expected when invalid username is given");
		} catch (const SecurityException &) {
			// Pass.
		}
		try {
			Client().connect(socketFilename, "test", "123456");
			fail("SecurityException expected when invalid password is given for an account with plain text password");
		} catch (const SecurityException &) {
			// Pass.
		}
		try {
			Client().connect(socketFilename, "test", "678900");
			fail("SecurityException expected when invalid password is given for an account with hashed password");
		} catch (const SecurityException &) {
			// Pass.
		}
	}
	
	TEST_METHOD(2) {
		// It supports hashed passwords.
		accountsDatabase->add("hashed_user", Account::createHash("67890"), true);
		Client().connect(socketFilename, "hashed_user", "67890"); // Should not throw exception.
	}
	
	TEST_METHOD(3) {
		// It disconnects the client if the client does not supply a username and
		// password within a time limit.
		Passenger::setLogLevel(-1);
		server->setLoginTimeout(40000);
		
		/* These can throw either an IOException or SystemException:
		 * - An IOException is raised when connect() encounters EOF.
		 * - But when connect() fails during the middle of a read()
		 *   or write() (e.g. because the server disconnects before
		 *   connect() is done writing), then SystemException is raised.
		 */
		
		try {
			// This client takes too much time on sending the username.
			SlowClient(50, 0).connect(socketFilename, "test", "12345");
			fail("IOException or SystemException expected (1).");
		} catch (const IOException &e) {
			// Pass.
		} catch (const SystemException &e) {
			// Pass.
		}
		
		try {
			// This client takes too much time on sending the password.
			SlowClient(0, 50).connect(socketFilename, "test", "12345");
			fail("IOException or SystemException expected (2).");
		} catch (const IOException &e) {
			// Pass.
		} catch (const SystemException &e) {
			// Pass.
		}
		
		try {
			// This client is fast enough at sending the username and
			// password individually, but the combined time is too long.
			SlowClient(25, 25).connect(socketFilename, "test", "12345");
			fail("IOException or SystemException expected (3).");
		} catch (const IOException &e) {
			// Pass.
		} catch (const SystemException &e) {
			// Pass.
		}
	}
	
	TEST_METHOD(4) {
		// It disconnects the client if it provides a username that's too large.
		char username[1024];
		memset(username, 'x', sizeof(username));
		username[sizeof(username) - 1] = '\0';
		try {
			Client().connect(socketFilename, username, "1234");
			fail("SecurityException expected");
		} catch (const SecurityException &e) {
			// Pass.
		}
	}
	
	TEST_METHOD(5) {
		// It disconnects the client if it provides a password that's too large.
		char password[1024];
		memset(password, 'x', sizeof(password));
		password[sizeof(password) - 1] = '\0';
		try {
			Client().connect(socketFilename, "test", password);
			fail("SecurityException expected");
		} catch (const SecurityException &e) {
			// Pass.
		}
	}
	
	TEST_METHOD(6) {
		// It notifies all handlers when a new client has connected.
		LoggingHandlerPtr handler1(new LoggingHandler());
		LoggingHandlerPtr handler2(new LoggingHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		Client().connect(socketFilename, "test", "12345");
		Client().connect(socketFilename, "test", "12345");
		
		usleep(10000); // Give the threads some time to do work.
		ensure_equals(handler1->clientsAccepted, 2);
		ensure_equals(handler2->clientsAccepted, 2);
	}
	
	TEST_METHOD(7) {
		// It notifies all handlers when a message is sent, but stops
		// at the first handler that returns true.
		LoggingHandlerPtr handler1(new LoggingHandler());
		LoggingHandlerPtr handler2(new LoggingHandler());
		LoggingHandlerPtr handler3(new LoggingHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		server->addHandler(handler3);
		handler1->returnValue = false;
		
		CustomClient c1, c2;
		c1.connect(socketFilename, "test", "12345");
		c1.sendText("hello");
		c1.sendText(" ");
		usleep(10000); // Give the thread some time to do work.
		
		c2.connect(socketFilename, "test", "12345");
		c2.sendText("world");
		usleep(10000); // Give the thread some time to do work.
		
		ensure_equals("(1)", handler1->receivedData, "hello world");
		ensure_equals("(2)", handler2->receivedData, "hello world");
		ensure_equals("(3)", handler3->receivedData, "");
	}
	
	TEST_METHOD(8) {
		// It does not close the connection if some, but not all of the handlers'
		// processMessage() methods return false.
		MessageServer::HandlerPtr handler1(new LoggingHandler());
		MessageServer::HandlerPtr handler2(new ProcessMessageReturnsFalseHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		
		CustomClient c;
		c.connect(socketFilename, "test", "12345");
		c.sendText("hi");
		usleep(10000); // Give the thread some time to do work.
		
		c.sendText("hi"); // Connection should still be valid.
	}
	
	TEST_METHOD(9) {
		// It closes the connection if all of the handlers'
		// processMessage() methods return false.
		MessageServer::HandlerPtr handler1(new ProcessMessageReturnsFalseHandler());
		MessageServer::HandlerPtr handler2(new ProcessMessageReturnsFalseHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		
		CustomClient c;
		c.connect(socketFilename, "test", "12345");
		c.sendText("hi");
		usleep(10000); // Give the thread some time to do work.
		
		try {
			c.sendText("hi");
			fail("SystemException expected.");
		} catch (const SystemException &e) {
			ensure_equals(e.code(), EPIPE);
		}
	}
	
	TEST_METHOD(10) {
		// The specific context as returned by the handler's newClient()
		// method is passed to processMessage().
		LoggingHandlerPtr handler1(new LoggingHandler());
		LoggingHandlerPtr handler2(new LoggingHandler());
		LoggingHandlerPtr handler3(new LoggingHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		server->addHandler(handler3);
		handler1->returnValue = false;
		handler2->returnValue = false;
		
		CustomClient c1, c2;
		
		handler1->id = 100;
		handler2->id = 101;
		c1.connect(socketFilename, "test", "12345");
		c1.sendText("hi");
		usleep(10000); // Give the thread some time to do work.
		ensure_equals(handler1->latestContext->id, 100);
		ensure_equals(handler2->latestContext->id, 101);
		
		handler1->id = 200;
		handler2->id = 201;
		c2.connect(socketFilename, "test", "12345");
		c2.sendText("hi");
		usleep(10000); // Give the thread some time to do work.
		ensure_equals(handler1->latestContext->id, 200);
		ensure_equals(handler2->latestContext->id, 201);
		
		c1.sendText("hi");
		usleep(10000); // Give the thread some time to do work.
		ensure_equals(handler1->latestContext->id, 100);
		ensure_equals(handler2->latestContext->id, 101);
	}
	
	TEST_METHOD(11) {
		// It notifies all handlers when a client is disconnected.
		LoggingHandlerPtr handler1(new LoggingHandler());
		LoggingHandlerPtr handler2(new LoggingHandler());
		server->addHandler(handler1);
		server->addHandler(handler2);
		{
			{
				Client().connect(socketFilename, "test", "12345");
			}
			usleep(10000); // Give the threads some time to do work.
			ensure_equals(handler1->clientsDisconnected, 1);
			ensure_equals(handler2->clientsDisconnected, 1);
			
			Client().connect(socketFilename, "test", "12345");
		}
		usleep(10000); // Give the threads some time to do work.
		ensure_equals(handler1->clientsDisconnected, 2);
		ensure_equals(handler2->clientsDisconnected, 2);
	}
}
