/* cpputest-client - CppUnit libnutclient active test

   Module for NUT `cpputest` runner, to check client-server interactions
   as part of NIT (NUT Integration Testing) suite and similar scenarios.
   This is an "active" NUT client talking to an `upsd` on $NUT_PORT,
   as opposed to nutclienttest.cpp which unit-tests the class API etc.
   in isolated-binary fashion.

   Copyright (C) 2022  Jim Klimov <jimklimov@gmail.com>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include "common.h"

/* Current CPPUnit offends the honor of C++98 */
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_EXIT_TIME_DESTRUCTORS || defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_GLOBAL_CONSTRUCTORS)
#pragma GCC diagnostic push
# ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_GLOBAL_CONSTRUCTORS
#  pragma GCC diagnostic ignored "-Wglobal-constructors"
# endif
# ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_EXIT_TIME_DESTRUCTORS
#  pragma GCC diagnostic ignored "-Wexit-time-destructors"
# endif
#endif

#include <cppunit/extensions/HelperMacros.h>
#include <stdexcept>
#include <cstdint>
#include <cstdlib>
#include <stdlib.h>

namespace nut {

class NutActiveClientTest : public CppUnit::TestFixture
{
	/* Note: is "friend" of nut::TcpClient class
	 * to test a few of its protected methods */

	CPPUNIT_TEST_SUITE( NutActiveClientTest );
		CPPUNIT_TEST( test_query_ver );
		CPPUNIT_TEST( test_list_ups );
		CPPUNIT_TEST( test_auth_user );
		CPPUNIT_TEST( test_auth_primary );
	CPPUNIT_TEST_SUITE_END();

private:
	/* Fed by caller via envvars: */
	uint16_t NUT_PORT = 0;
	std::string NUT_USER = "";
	std::string NUT_PASS = "";
	std::string NUT_PRIMARY_DEVICE = "";
	std::string NUT_SETVAR_DEVICE = "";

public:
	void setUp() override;
	void tearDown() override;

	void test_query_ver();
	void test_list_ups();
	void test_auth_user();
	void test_auth_primary();
};

// Registers the fixture into the 'registry'
CPPUNIT_TEST_SUITE_REGISTRATION( NutActiveClientTest );

} // namespace nut {}

#ifndef _NUTCLIENTTEST_BUILD
# define _NUTCLIENTTEST_BUILD 1
#endif

#include "../clients/nutclient.h"
#include "../clients/nutclientmem.h"

namespace nut {

extern "C" {
strarr stringset_to_strarr(const std::set<std::string>& strset);
strarr stringvector_to_strarr(const std::vector<std::string>& strset);
} // extern "C"

void NutActiveClientTest::setUp()
{
	/* NUT_PORT etc. are provided by external test suite driver */
	char * s;

	s = std::getenv("NUT_PORT");
	if (s) {
		long l = atol(s);
		if (l < 1 || l > 65535) {
			throw std::runtime_error("NUT_PORT specified by caller is out of range");
		}
		NUT_PORT = static_cast<uint16_t>(l);
	} else {
		throw std::runtime_error("NUT_PORT not specified by caller, NIT should call this test");
	}

	s = std::getenv("NUT_USER");
	if (s) {
		NUT_USER = s;
	} // else stays empty

	s = std::getenv("NUT_PASS");
	if (s) {
		NUT_PASS = s;
	} // else stays empty

	s = std::getenv("NUT_PRIMARY_DEVICE");
	if (s) {
		NUT_PRIMARY_DEVICE = s;
	} // else stays empty

	s = std::getenv("NUT_SETVAR_DEVICE");
	if (s) {
		NUT_SETVAR_DEVICE = s;
	} // else stays empty
}

void NutActiveClientTest::tearDown()
{
}

void NutActiveClientTest::test_query_ver() {
	nut::TcpClient c("localhost", NUT_PORT);
	std::string s;

	std::cerr << "[D] C++ NUT Client lib test running against Data Server at: "
		<< c.getHost() << ':' << c.getPort() << std::endl;

	CPPUNIT_ASSERT_MESSAGE(
		"TcpClient is not connected after constructor",
		c.isConnected());

	/* Note: generic client code can not use protected methods
	 * like low-level sendQuery(), list(), get() and some more,
	 * but this NutActiveClientTest is a friend of TcpClient:
	 */
	s = c.sendQuery("VER");
	std::cerr << "[D] Got Data Server VER: " << s << std::endl;

	try {
		s = c.sendQuery("PROTVER");
		std::cerr << "[D] Got PROTVER: " << s << std::endl;
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Did not get PROTVER: " << ex.what() << std::endl;
	}

	try {
		s = c.sendQuery("NETVER");
		std::cerr << "[D] Got NETVER (obsolete): " << s << std::endl;
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Did not get NETVER (obsolete): " << ex.what() << std::endl;
	}

	try {
		c.logout();
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Could not get LOGOUT: " << ex.what() << std::endl;
	}

	try {
		c.disconnect();
	}
	catch(nut::NutException& ex)
	{
		/* NUT_UNUSED_VARIABLE(ex); */
		std::cerr << "[D] Could not get disconnect(): " << ex.what() << std::endl;
	}
}

void NutActiveClientTest::test_list_ups() {
	nut::TcpClient c("localhost", NUT_PORT);
	std::set<std::string> devs;
	bool noException = true;

	try {
		devs = c.getDeviceNames();
		std::cerr << "[D] Got device list (" << devs.size() << "): [";
		for (std::set<std::string>::iterator it = devs.begin();
			it != devs.end(); it++
		) {
			if (it != devs.begin()) {
				std::cerr << ", ";
			}
			std::cerr << '"' << *it << '"';
		}
		std::cerr << "]" << std::endl;
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Could not device list: " << ex.what() << std::endl;
		noException = false;
	}

	c.logout();
	c.disconnect();

	CPPUNIT_ASSERT_MESSAGE(
		"Failed to list UPS with TcpClient: threw NutException",
		noException);
}

void NutActiveClientTest::test_auth_user() {
	if (NUT_USER.empty()) {
		std::cerr << "[D] SKIPPING test_auth_user()" << std::endl;
		return;
	}

	nut::TcpClient c("localhost", NUT_PORT);
	bool noException = true;
	try {
		c.authenticate(NUT_USER, NUT_PASS);
		std::cerr << "[D] Authenticated without exceptions" << std::endl;
		/* Note: no high hopes here, credentials are checked by server
		 * when running critical commands, not at auth request itself */
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Could not authenticate as a simple user: " << ex.what() << std::endl;
		noException = false;
	}

	if (!NUT_SETVAR_DEVICE.empty()) {
		try {
			TrackingResult tres;
			TrackingID tid;
			int i;
			std::string nutVar = "ups.status"; /* Has a risk of flip-flop with NIT dummy setup */
			std::string s1 = c.getDeviceVariableValue(NUT_SETVAR_DEVICE, nutVar)[0];
			std::string sTest = s1 + "-test";

			std::cerr << "[D] Got initial device '" << NUT_SETVAR_DEVICE
				<< "' variable '" << nutVar << "' value: " << s1 << std::endl;
			CPPUNIT_ASSERT_MESSAGE(
				"Did not expect empty value here",
				!s1.empty());

			tid = c.setDeviceVariable(NUT_SETVAR_DEVICE, nutVar, sTest);
			while ( (tres = c.getTrackingResult(tid)) == PENDING) {
				usleep(100);
			}
			if (tres != SUCCESS) {
				std::cerr << "[D] Failed to set device variable: "
					<< "tracking result is " << tres << std::endl;
				noException = false;
			}
			/* Check what we got after set */
			/* Note that above we told the server to tell the driver
			 * to set a dstate entry; below we ask the server to ask
			 * the driver and relay the answer to us. The dummy-ups
			 * driver may also be in a sleeping state between cycles.
			 * Data propagation may be not instantaneous, so we loop
			 * for a while to see the expected value (or give up).
			 */
			std::string s2;
			for (i = 0; i < 100 ; i++) {
				s2 = c.getDeviceVariableValue(NUT_SETVAR_DEVICE, nutVar)[0];
				if (s2 == sTest)
					break;
				usleep(100000);
			}
			std::cerr << "[D] Read back: " << s2
				<< " after " << (100 * i) << "msec"
				<< std::endl;

			/* Fix it back */
			tid = c.setDeviceVariable(NUT_SETVAR_DEVICE, nutVar, s1);
			while ( (tres = c.getTrackingResult(tid)) == PENDING) {
				usleep(100);
			}
			if (tres != SUCCESS) {
				std::cerr << "[D] Failed to set device variable: "
					<< "tracking result is " << tres << std::endl;
				noException = false;
			}
			std::string s3;
			for (i = 0; i < 100 ; i++) {
				s3 = c.getDeviceVariableValue(NUT_SETVAR_DEVICE, nutVar)[0];
				if (s3 == s1)
					break;
				usleep(100000);
			}
			std::cerr << "[D] Read back: " << s3
				<< " after " << (100 * i) << "msec"
				<< std::endl;

			if (s3 != s1) {
				std::cerr << "[D] Final device variable value '" << s3
					<< "' differs from original '" << s1
					<< "'" << std::endl;
				noException = false;
			}

			if (s2 == s1) {
				std::cerr << "[D] Tweaked device variable value '" << s2
					<< "' does not differ from original '" << s1
					<< "'" << std::endl;
				noException = false;
			}

			if (noException) {
				std::cerr << "[D] Tweaked device variable value OK" << std::endl;
			}
		}
		catch(nut::NutException& ex)
		{
			std::cerr << "[D] Failed to set device variable: "
				<< ex.what() << std::endl;
			noException = false;
		}
	} else {
		std::cerr << "[D] SKIPPING test_auth_user() active test "
			<< "(got no NUT_SETVAR_DEVICE to poke)" << std::endl;
	}

	c.logout();
	c.disconnect();

	CPPUNIT_ASSERT_MESSAGE(
		"Failed to auth as user with TcpClient or tweak device variable",
		noException);
}

void NutActiveClientTest::test_auth_primary() {
	if (NUT_USER.empty() || NUT_PRIMARY_DEVICE.empty()) {
		std::cerr << "[D] SKIPPING test_auth_primary()" << std::endl;
		return;
	}

	nut::TcpClient c("localhost", NUT_PORT);
	bool noException = true;
	try {
		c.authenticate(NUT_USER, NUT_PASS);
		std::cerr << "[D] Authenticated without exceptions" << std::endl;
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] Could not authenticate as an upsmon primary user: "
			<< ex.what() << std::endl;
		noException = false;
	}

	try {
		Device d = c.getDevice(NUT_PRIMARY_DEVICE);
		bool gotPrimary = false;
		bool gotMaster = false;

		try {
			c.deviceMaster(NUT_PRIMARY_DEVICE);
			gotMaster = true;
			std::cerr << "[D] Elevated as MASTER without exceptions" << std::endl;
		}
		catch(nut::NutException& ex)
		{
			std::cerr << "[D] Could not elevate as MASTER for "
				<< "NUT_PRIMARY_DEVICE " << NUT_PRIMARY_DEVICE << ": "
				<< ex.what() << std::endl;
		}

		try {
			c.devicePrimary(NUT_PRIMARY_DEVICE);
			gotPrimary = true;
			std::cerr << "[D] Elevated as PRIMARY without exceptions" << std::endl;
		}
		catch(nut::NutException& ex)
		{
			std::cerr << "[D] Could not elevate as PRIMARY for "
				<< "NUT_PRIMARY_DEVICE " << NUT_PRIMARY_DEVICE << ": "
				<< ex.what() << std::endl;
		}

		if (!gotMaster && !gotPrimary)
			noException = false;
	}
	catch(nut::NutException& ex)
	{
		std::cerr << "[D] NUT_PRIMARY_DEVICE " << NUT_PRIMARY_DEVICE
			<< " not found on Data Server: "
			<< ex.what() << std::endl;
		noException = false;
	}

	c.logout();
	c.disconnect();

	CPPUNIT_ASSERT_MESSAGE(
		"Failed to auth as user with TcpClient: threw NutException",
		noException);
}

} // namespace nut {}

#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_EXIT_TIME_DESTRUCTORS || defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_GLOBAL_CONSTRUCTORS)
#pragma GCC diagnostic pop
#endif
