/**
 * @file   lllogin_test.cpp
 * @author Mark Palange
 * @date   2009-02-26
 * @brief  Tests of lllogin.cpp.
 * 
 * $LicenseInfo:firstyear=2009&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2009-2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#if LL_WINDOWS
#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
#endif

// Precompiled header
#include "linden_common.h"
// associated header
#include "../lllogin.h"
// STL headers
// std headers
#include <iostream>
// external library headers
// other Linden headers
#include "llsd.h"
#include "../../../test/lltut.h"
//#define DEBUG_ON
#include "../../../test/debug.h"
#include "llevents.h"
#include "stringize.h"

#if LL_WINDOWS
#define skipwin(arg) skip(arg)
#define skipmac(arg)
#define skiplinux(arg)
#elif LL_DARWIN
#define skipwin(arg)
#define skipmac(arg) skip(arg)
#define skiplinux(arg)
#elif LL_LINUX
#define skipwin(arg)
#define skipmac(arg)
#define skiplinux(arg) skip(arg)
#endif

/*****************************************************************************
*   Helper classes
*****************************************************************************/
// This is a listener to receive results from lllogin.
class LoginListener: public LLEventTrackable
{
	std::string mName;
	LLSD mLastEvent;
    Debug mDebug;
public:
	LoginListener(const std::string& name) : 
		mName(name),
        mDebug(llformat("%p", this))
	{}

	bool call(const LLSD& event)
	{
		mDebug(STRINGIZE("LoginListener called!: " << event));
		
		mLastEvent = event;
		return false;
	}

    LLBoundListener listenTo(LLEventPump& pump)
    {
        return pump.listen(mName, boost::bind(&LoginListener::call, this, _1));
	}

	LLSD lastEvent() const { return mLastEvent; }

    friend std::ostream& operator<<(std::ostream& out, const LoginListener& listener)
    {
        return out << "LoginListener(" << listener.mName << ')';
    }
};

class LLAresListener: public LLEventTrackable
{
	std::string mName;
	LLSD mEvent;
	bool mImmediateResponse;
	bool mMultipleURIResponse;
    Debug mDebug;
	
public:
	LLAresListener(const std::string& name, 
				   bool i = false,
				   bool m = false
				   ) : 
		mName(name),
		mImmediateResponse(i),
		mMultipleURIResponse(m),
        mDebug(llformat("%p", this))
	{}

	bool handle_event(const LLSD& event)
	{
		mDebug(STRINGIZE("LLAresListener called!: " << event));
		mEvent = event;
		if(mImmediateResponse)
		{
			sendReply();
		}
		return false;
	}

	void sendReply()
	{
		if(mEvent["op"].asString() == "rewriteURI")
		{
			LLSD result;
			if(mMultipleURIResponse)
			{
				result.append(LLSD("login.foo.com"));
			}
			result.append(mEvent["uri"]);
			LLEventPumps::instance().obtain(mEvent["reply"]).post(result);
		}
	}

	LLBoundListener listenTo(LLEventPump& pump)
    {
        return pump.listen(mName, boost::bind(&LLAresListener::handle_event, this, _1));
	}

    friend std::ostream& operator<<(std::ostream& out, const LLAresListener& listener)
    {
        return out << "LLAresListener(" << listener.mName << ')';
    }
};

class LLXMLRPCListener: public LLEventTrackable
{
	std::string mName;
	LLSD mEvent;
	bool mImmediateResponse;
	LLSD mResponse;
    Debug mDebug;

public:
	LLXMLRPCListener(const std::string& name, 
					 bool i = false,
					 const LLSD& response = LLSD()
					 ) : 
		mName(name),
		mImmediateResponse(i),
		mResponse(response),
        mDebug(llformat("%p", this))
	{
		if(mResponse.isUndefined())
		{
			mResponse["status"] = "Complete"; // StatusComplete
			mResponse["errorcode"] = 0;
			mResponse["error"] = "dummy response";
			mResponse["transfer_rate"] = 0;
			mResponse["responses"]["login"] = true;
		}
	}

	void setResponse(const LLSD& r) 
	{ 
		mResponse = r; 
	}

	bool handle_event(const LLSD& event)
	{
		mDebug(STRINGIZE("LLXMLRPCListener called!: " << event));
		mEvent = event;
		if(mImmediateResponse)
		{
			sendReply();
		}
		return false;
	}

	void sendReply()
	{
		LLEventPumps::instance().obtain(mEvent["reply"]).post(mResponse);
	}

	LLBoundListener listenTo(LLEventPump& pump)
    {
        return pump.listen(mName, boost::bind(&LLXMLRPCListener::handle_event, this, _1));
	}

    friend std::ostream& operator<<(std::ostream& out, const LLXMLRPCListener& listener)
    {
        return out << "LLXMLRPCListener(" << listener.mName << ')';
    }
};

/*****************************************************************************
*   TUT
*****************************************************************************/
namespace tut
{
    struct llviewerlogin_data
    {
		llviewerlogin_data() :
            pumps(LLEventPumps::instance())
		{}
		LLEventPumps& pumps;
	};

    typedef test_group<llviewerlogin_data> llviewerlogin_group;
    typedef llviewerlogin_group::object llviewerlogin_object;
    llviewerlogin_group llviewerlogingrp("LLViewerLogin");

    template<> template<>
    void llviewerlogin_object::test<1>()
    {
        DEBUG;
		// Testing login with immediate responses from Ares and XMLPRC
		// The response from both requests will come before the post request exits.
		// This tests an edge case of the login state handling.
		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump

		bool respond_immediately = true;
		// Have 'dummy ares' respond immediately. 
		LLAresListener dummyLLAres("dummy_llares", respond_immediately);
		dummyLLAres.listenTo(llaresPump);

		// Have dummy XMLRPC respond immediately.
		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc", respond_immediately);
		dummyXMLRPC.listenTo(xmlrpcPump);

		LLLogin login;

		LoginListener listener("test_ear");
		listener.listenTo(login.getEventPump());

		LLSD credentials;
		credentials["first"] = "foo";
		credentials["last"] = "bar";
		credentials["passwd"] = "secret";

		login.connect("login.bar.com", credentials);

		ensure_equals("Online state", listener.lastEvent()["state"].asString(), "online");
	}

    template<> template<>
    void llviewerlogin_object::test<2>()
    {
        DEBUG;
		// Tests a successful login in with delayed responses. 
		// Also includes 'failure' that cause the login module
		// to re-attempt connection, once from a basic failure
		// and once from the 'indeterminate' response.

		set_test_name("LLLogin multiple srv uris w/ success");

		// Testing normal login procedure.
		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump

		bool respond_immediately = false;
		bool multiple_addresses = true;
		LLAresListener dummyLLAres("dummy_llares", respond_immediately, multiple_addresses);
		dummyLLAres.listenTo(llaresPump);

		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
		dummyXMLRPC.listenTo(xmlrpcPump);

		LLLogin login;

		LoginListener listener("test_ear");
		listener.listenTo(login.getEventPump());

		LLSD credentials;
		credentials["first"] = "foo";
		credentials["last"] = "bar";
		credentials["passwd"] = "secret";

		login.connect("login.bar.com", credentials);

		ensure_equals("SRV state", listener.lastEvent()["change"].asString(), "srvrequest"); 

		dummyLLAres.sendReply();

		// Test Authenticating State prior to first response.
		ensure_equals("Auth state 1", listener.lastEvent()["change"].asString(), "authenticating"); 
		ensure_equals("Attempt 1", listener.lastEvent()["data"]["attempt"].asInteger(), 1); 
		ensure_equals("URI 1", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.foo.com"); 

		// First send emulated LLXMLRPCListener failure,
		// this should return login to the authenticating step and increase the attempt 
		// count.
		LLSD data;
		data["status"] = "OtherError"; 
		data["errorcode"] = 0;
		data["error"] = "dummy response";
		data["transfer_rate"] = 0;
		dummyXMLRPC.setResponse(data);
		dummyXMLRPC.sendReply();

		ensure_equals("Fail back to authenticate 1", listener.lastEvent()["change"].asString(), "authenticating"); 
		ensure_equals("Attempt 2", listener.lastEvent()["data"]["attempt"].asInteger(), 2); 
		ensure_equals("URI 2", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.bar.com"); 

		// Now send the 'indeterminate' response.
		data.clear();
		data["status"] = "Complete"; // StatusComplete
		data["errorcode"] = 0;
		data["error"] = "dummy response";
		data["transfer_rate"] = 0;
		data["responses"]["login"] = "indeterminate";
		data["responses"]["next_url"] = "login.indeterminate.com";			
		data["responses"]["next_method"] = "test_login_method"; 			
		dummyXMLRPC.setResponse(data);
		dummyXMLRPC.sendReply();

		ensure_equals("Fail back to authenticate 2", listener.lastEvent()["change"].asString(), "authenticating"); 
		ensure_equals("Attempt 3", listener.lastEvent()["data"]["attempt"].asInteger(), 3); 
		ensure_equals("URI 3", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.indeterminate.com"); 
		ensure_equals("Method 3", listener.lastEvent()["data"]["request"]["method"].asString(), "test_login_method"); 

		// Finally let the auth succeed.
		data.clear();
		data["status"] = "Complete"; // StatusComplete
		data["errorcode"] = 0;
		data["error"] = "dummy response";
		data["transfer_rate"] = 0;
		data["responses"]["login"] = "true";
		dummyXMLRPC.setResponse(data);
		dummyXMLRPC.sendReply();

		ensure_equals("Success state", listener.lastEvent()["state"].asString(), "online");

		login.disconnect();

		ensure_equals("Disconnected state", listener.lastEvent()["state"].asString(), "offline");
	}

    template<> template<>
    void llviewerlogin_object::test<3>()
    {
        DEBUG;
		// Test completed response, that fails to login.
		set_test_name("LLLogin valid response, failure (eg. bad credentials)");

		// Testing normal login procedure.
		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump

		LLAresListener dummyLLAres("dummy_llares");
		dummyLLAres.listenTo(llaresPump);

		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
		dummyXMLRPC.listenTo(xmlrpcPump);

		LLLogin login;
		LoginListener listener("test_ear");
		listener.listenTo(login.getEventPump());

		LLSD credentials;
		credentials["first"] = "who";
		credentials["last"] = "what";
		credentials["passwd"] = "badpasswd";

		login.connect("login.bar.com", credentials);

		ensure_equals("SRV state", listener.lastEvent()["change"].asString(), "srvrequest"); 

		dummyLLAres.sendReply();

		ensure_equals("Auth state", listener.lastEvent()["change"].asString(), "authenticating"); 

		// Send the failed auth request reponse
		LLSD data;
		data["status"] = "Complete";
		data["errorcode"] = 0;
		data["error"] = "dummy response";
		data["transfer_rate"] = 0;
		data["responses"]["login"] = "false";
		dummyXMLRPC.setResponse(data);
		dummyXMLRPC.sendReply();

		ensure_equals("Failed to offline", listener.lastEvent()["state"].asString(), "offline");
	}

    template<> template<>
    void llviewerlogin_object::test<4>()
    {
        DEBUG;
		// Test incomplete response, that end the attempt.
		set_test_name("LLLogin valid response, failure (eg. bad credentials)");

		// Testing normal login procedure.
		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump

		LLAresListener dummyLLAres("dummy_llares");
		dummyLLAres.listenTo(llaresPump);

		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
		dummyXMLRPC.listenTo(xmlrpcPump);

		LLLogin login;
		LoginListener listener("test_ear");
		listener.listenTo(login.getEventPump());

		LLSD credentials;
		credentials["first"] = "these";
		credentials["last"] = "don't";
		credentials["passwd"] = "matter";

		login.connect("login.bar.com", credentials);

		ensure_equals("SRV state", listener.lastEvent()["change"].asString(), "srvrequest"); 

		dummyLLAres.sendReply();

		ensure_equals("Auth state", listener.lastEvent()["change"].asString(), "authenticating"); 

		// Send the failed auth request reponse
		LLSD data;
		data["status"] = "OtherError";
		data["errorcode"] = 0;
		data["error"] = "dummy response";
		data["transfer_rate"] = 0;
		dummyXMLRPC.setResponse(data);
		dummyXMLRPC.sendReply();

		ensure_equals("Failed to offline", listener.lastEvent()["state"].asString(), "offline");
	}

	template<> template<>
    void llviewerlogin_object::test<5>()
    {
        DEBUG;
		// Test SRV request timeout.
		set_test_name("LLLogin SRV timeout testing");

		// Testing normal login procedure.
		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.

		LLAresListener dummyLLAres("dummy_llares");
		dummyLLAres.listenTo(llaresPump);

		LLLogin login;
		LoginListener listener("test_ear");
		listener.listenTo(login.getEventPump());

		LLSD credentials;
		credentials["first"] = "these";
		credentials["last"] = "don't";
		credentials["passwd"] = "matter";
		credentials["cfg_srv_timeout"] = 0.0f;

		login.connect("login.bar.com", credentials);

		ensure_equals("SRV State", listener.lastEvent()["change"].asString(), "srvrequest"); 

		// Get the mainloop eventpump, which needs a pinging in order to drive the 
		// SRV timeout.
		LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop"));
		LLSD frame_event;
		mainloop.post(frame_event);

		// In this state we have NOT sent a reply from LLAresListener -- in
		// fact there's no such object. Nonetheless, we expect the timeout to
		// have stepped the login module forward to try to authenticate with
		// the original URI.
		ensure_equals("Auth state", listener.lastEvent()["change"].asString(), "authenticating"); 
		ensure_equals("Attempt", listener.lastEvent()["data"]["attempt"].asInteger(), 1); 
		ensure_equals("URI", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.bar.com");

		// EXT-4193: if the SRV reply isn't lost but merely late, and if it
		// arrives just at the moment we're expecting the XMLRPC reply, the
		// original code got confused and crashed. Drive that case here. We
		// observe that without the fix, this call DOES repro.
		dummyLLAres.sendReply();
	}
}
