/**
 * @file reg_test_3504-change_user-t.cpp
 * @brief This test checks the new implementation for 'COM_CHANGE_USER'
 *  introduced in issue #3504. The test connects using different authentication methods:
 *    - 'mysql_clear_password'
 *    - 'mysql_native_password'
 *    - 'caching_sha2_password'
 *  It also checks that the following options are handled correctly:
 *    - With and without SSL.
 *    - Using same and different users.
 *    - Hashed and non-hashed user passwords are correctly handled.
 *  The test verifies that both, the initial connection and the later 'mysql_change_user' are
 *  properly executed. Connections are performed using 'libmysqlclient' and 'libmariadb'.
 * @details For making this possible the test uses two helper binaries, which are the ones performing the
 *  connection to ProxySQL, and communicates to them through a payload format that is specified in this helper
 *  tests files:
 *    - 'reg_test_3504-change_user_libmysql_helper.cpp'
 *    - 'reg_test_3504-change_user_libmariadb_helper.cpp'
 */

#include <cstring>
#include <vector>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <unistd.h>

#include "mysql.h"

#include "command_line.h"
#include "json.hpp"
#include "tap.h"
#include "utils.h"

using nlohmann::json;
using std::string;
using std::vector;

struct test_opts {
	string auth;
	bool use_ssl;
	bool change_user;
	bool hashed_pass;
	bool inv_pass;
};

const vector<string> client_req_auths {
	"mysql_clear_password",
	"mysql_native_password",
	"caching_sha2_password"
};

vector<test_opts> gen_tests_defs() {
	// Gen all option permutations - SSL, different user, and hashed user passwords.
	const auto flags_perms { get_all_bin_vec(4) };

	// Use all options for each supported auth method
	vector<test_opts> res {};

	for (const auto& flags : flags_perms) {
		for (const string& auth : client_req_auths) {
			res.push_back({auth, flags[0], flags[1], flags[2], flags[3]});
		}
	}

	return res;
}

const string PRIM_USER { get_env_str("TAP_CHANGE_USER__PRIM_USER", "sbtest1") };
const string SECD_USER { get_env_str("TAP_CHANGE_USER__SECD_USER", "root") };

const char LOAD_USERS_TO_RUNTIME[] { "LOAD MYSQL USERS TO RUNTIME" };

int update_user_pass(MYSQL* admin, const string& user, const string& pass) {
	int rc = mysql_query_t(admin,
		("UPDATE mysql_users SET password=" + pass + "" + " WHERE username='" + user + "'").c_str()
	);
	if (rc) {
		diag(
			"Failed to set HASHED user pass. Aborting check   user='%s' error='%s'",
			user.c_str(), mysql_error(admin)
		);
	}

	return rc;
}

string gen_inv_pass(const string& pass) {
	string rnd_str { random_string(rand() % 60 + 1) };

	while (rnd_str == pass) {
		rnd_str = random_string(rand() % 60 + 1);
	}

	return rnd_str;
}

const string opts_to_string(const test_opts& opts) {
	nlohmann::json j_opts {};

	j_opts["auth"] = opts.auth;
	j_opts["use_ssl"] = opts.use_ssl;
	j_opts["mix_users"] = opts.change_user;
	j_opts["hashed_pass"] = opts.hashed_pass;
	j_opts["inv_pass"] = opts.inv_pass;

	return j_opts.dump();
}

void perform_helper_test(
	MYSQL* admin,
	const CommandLine& cl,
	const std::string& helper_path,
	const test_opts& opts
) {
	diag("Preparing call to helper   opts='%s'", opts_to_string(opts).c_str());

	std::string result {};

	if (opts.hashed_pass) {
		for (const string& user : { PRIM_USER, SECD_USER }) {
			int rc = update_user_pass(admin, user, "MYSQL_NATIVE_PASSWORD('" + user + "')");
			if (rc) { return; }
		}
	} else {
		for (const string& user : { PRIM_USER, SECD_USER }) {
			int rc = update_user_pass(admin, user, "'" + user + "'");
			if (rc) { return; }
		}
	}

	int rc = mysql_query_t(admin, "LOAD MYSQL USERS TO RUNTIME");
	if (rc) {
		diag("Failed to execute query. Aborting check   error='%s'", mysql_error(admin));
		return;
	}

	nlohmann::json input_json {};
	input_json["user"] = PRIM_USER;
	input_json["pass"] = PRIM_USER;
	input_json["ch_user"] = SECD_USER;
	input_json["ch_pass"] = opts.inv_pass ? gen_inv_pass(SECD_USER) : SECD_USER;
	input_json["auth"] = opts.auth;
	input_json["charset"] = "";
	input_json["host"] = cl.host;
	input_json["port"] = cl.port;
	input_json["SSL"] = opts.use_ssl;
	input_json["CHANGE_USER"] = opts.change_user;

	std::string input_str { input_json.dump() };

	diag("Calling test helper   params='%s'", input_json.dump().c_str());

	std::vector<const char*> v_argv { helper_path.c_str(), input_str.c_str() };
	int res = execvp(helper_path, v_argv, result);

	diag("Result from helper, err_code: '%d', result: '%s'", res, result.c_str());

	std::string err_msg {};
	int exp_switching_auth_type = -1;
	int act_switching_auth_type = 0;
	std::string def_auth_plugin {};
	bool act_SSL_val;
	std::vector<std::string> exp_ch_usernames {};

	if (opts.change_user) {
		exp_ch_usernames = { "root", "sbtest1", "root" };
	} else {
		exp_ch_usernames = { "sbtest1", "sbtest1", "sbtest1" };
	}

	std::vector<std::string> act_ch_usernames {};

	try {
		nlohmann::json output_res = nlohmann::json::parse(result);

		if (output_res.contains("err_msg")) {
			err_msg = output_res.at("err_msg");
		}

		act_switching_auth_type = output_res.at("switching_auth_type");
		def_auth_plugin = output_res.at("def_auth_plugin");
		act_SSL_val = output_res.at("ssl_enabled");

		if (opts.auth == "mysql_clear_password") {
			exp_switching_auth_type = -1;
		} else if (opts.auth == "mysql_native_password") {
			exp_switching_auth_type = -1;
		} else if (opts.auth == "caching_sha2_password") {
			exp_switching_auth_type = 0;
		}

		act_ch_usernames.push_back(output_res.at("client_com_change_user_1"));
		act_ch_usernames.push_back(output_res.at("client_com_change_user_2"));
		act_ch_usernames.push_back(output_res.at("client_com_change_user_3"));
	} catch (const std::exception& ex) {
		diag("Invalid JSON result from helper, parsing failed: '%s'", ex.what());
	}

	// Failure with invalid CHANGE_USER pass only for real change user ops - src_user != tg_user.
	if (!opts.inv_pass || !opts.change_user) {
		std::string exp_user_names_str =
			std::accumulate(exp_ch_usernames.begin(), exp_ch_usernames.end(), std::string(),
			[](const std::string& str, const std::string& splice) -> std::string {
				return str + (str.length() > 0 ? "," : "") + splice;
			});
		std::string act_user_names_str =
			std::accumulate(act_ch_usernames.begin(), act_ch_usernames.end(), std::string(),
			[](const std::string& str, const std::string& splice) -> std::string {
				return str + (str.length() > 0 ? "," : "") + splice;
			});

		ok(
			(exp_switching_auth_type == act_switching_auth_type) &&
			(opts.use_ssl == act_SSL_val) && err_msg.empty() &&
			exp_ch_usernames == act_ch_usernames,
			"Connect and COM_CHANGE_USER should work for the supplied values.\n"
			" + Expected: (client_auth_plugin='%s', switching_auth_type='%d', SSL='%d', usernames=['%s']),\n"
			" + Actual: (client_auth_plugin='%s', switching_auth_type='%d', SSL='%d, usernames=['%s']').\n"
			" Error message: %s.",
			opts.auth.c_str(), exp_switching_auth_type, opts.use_ssl, exp_user_names_str.c_str(),
			def_auth_plugin.c_str(), act_switching_auth_type, act_SSL_val, act_user_names_str.c_str(),
			err_msg.c_str()
		);
	} else {
		const string::size_type f_it { err_msg.find("Failed to change user") };
		ok(
			res != 0 && !err_msg.empty() && f_it != string::npos,
			"COM_CHANGE_USER should fail with 'Access denied' for invalid creds   res=%d err='%s'",
			res, err_msg.c_str()
		);
	}
}

int main(int argc, char** argv) {
	CommandLine cl;

	if (cl.getEnv()) {
		diag("Failed to get the required environmental variables.");
		return EXIT_FAILURE;
	}

	srand(time(NULL));
	MYSQL* admin = mysql_init(NULL);

	if (
		!mysql_real_connect(
			admin, cl.admin_host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0
		)
	) {
		fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
		return EXIT_FAILURE;
	}

	// TODO: This test now only checks support for 'mysql_native_password'. This should be changed once
	// 'COM_CHANGE_USER' is supported for 'caching_sha2_password'. See #4618.
	MYSQL_QUERY(admin, "SET mysql-default_authentication_plugin='mysql_native_password'");
	MYSQL_QUERY(admin, "SET mysql-have_ssl='true'");
	MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME");

	const vector<test_opts> tests_defs { gen_tests_defs() };
	plan(tests_defs.size() * 2);

	// Verbose test header
	diag("================================================================================");
	diag("Test: reg_test_3504-change_user");
	diag("================================================================================");
	diag("This test checks the COM_CHANGE_USER implementation introduced in issue #3504.");
	diag("It verifies that ProxySQL correctly handles user switching during a connection.");
	diag("");
	diag("Test scenarios:");
	diag("  - Multiple authentication methods: mysql_clear_password, mysql_native_password,");
	diag("    caching_sha2_password");
	diag("  - With and without SSL connections");
	diag("  - Same user and different user switching");
	diag("  - Hashed and non-hashed user passwords");
	diag("  - Invalid password handling");
	diag("");
	diag("Configuration:");
	diag("  - ProxySQL admin host: %s", cl.admin_host);
	diag("  - ProxySQL admin port: %d", cl.admin_port);
	diag("  - ProxySQL data host: %s", cl.host);
	diag("  - ProxySQL data port: %d", cl.port);
	diag("  - Primary user: %s", PRIM_USER.c_str());
	diag("  - Secondary user: %s", SECD_USER.c_str());
	diag("  - Total test cases: %zu (per helper)", tests_defs.size());
	diag("================================================================================");
	diag("");

	diag("Starting tests for helper 'reg_test_3504-change_user_libmysql_helper'\n");

	std::string libmysql_helper_path {
		std::string { cl.workdir } + "reg_test_3504-change_user_libmysql_helper"
	};
	for (const auto& test_opts : tests_defs) {
		perform_helper_test(admin, cl, libmysql_helper_path, test_opts);
	}

	std::cout << "\n";
	diag("Starting tests for helper 'reg_test_3504-change_user_libmariadb_helper'\n");

	std::string libmariadb_helper_path {
		std::string { cl.workdir } + "reg_test_3504-change_user_libmariadb_helper"
	};
	for (const auto& test_opts : tests_defs) {
		perform_helper_test(admin, cl, libmariadb_helper_path, test_opts);
	}

	mysql_close(admin);

	return exit_status();
}
