/**
 * @file auth_unit-t.cpp
 * @brief Unit tests for MySQL_Authentication and PgSQL_Authentication.
 *
 * Tests the authentication subsystem in isolation without a running
 * ProxySQL instance. Covers:
 *   - Core CRUD: add, lookup, del, exists
 *   - Credential management: SHA1, clear-text passwords
 *   - Connection counting and max_connections enforcement
 *   - Bulk operations: set_all_inactive, remove_inactives, reset
 *   - Runtime checksums
 *   - Memory tracking
 *   - Frontend vs backend separation
 *   - PgSQL Authentication parity
 *
 * @see Phase 2.2 of the Unit Testing Framework (GitHub issue #5474)
 */

#include "tap.h"
#include "test_globals.h"
#include "test_init.h"

#include "proxysql.h"
#include "MySQL_Authentication.hpp"
#include "PgSQL_Authentication.h"

#include <cstring>
#include <openssl/sha.h>

// Extern declarations for Glo* pointers (defined in test_globals.cpp)
extern MySQL_Authentication *GloMyAuth;
extern PgSQL_Authentication *GloPgAuth;

// ============================================================================
// Helper: add a MySQL frontend user with common defaults
// ============================================================================
static bool mysql_add_frontend(MySQL_Authentication *auth,
	const char *user, const char *pass,
	int default_hg = 0, int max_conn = 100)
{
	return auth->add(
		(char *)user, (char *)pass, USERNAME_FRONTEND,
		false,            // use_ssl
		default_hg,       // default_hostgroup
		(char *)"",       // default_schema
		false,            // schema_locked
		false,            // transaction_persistent
		false,            // fast_forward
		max_conn,         // max_connections
		(char *)"",       // attributes
		(char *)""        // comment
	);
}

// ============================================================================
// Helper: add a MySQL backend user
// ============================================================================
static bool mysql_add_backend(MySQL_Authentication *auth,
	const char *user, const char *pass,
	int default_hg = 0, int max_conn = 100)
{
	return auth->add(
		(char *)user, (char *)pass, USERNAME_BACKEND,
		false, default_hg, (char *)"", false, false, false,
		max_conn, (char *)"", (char *)""
	);
}

// ============================================================================
// Helper: add a PgSQL frontend user
// ============================================================================
static bool pgsql_add_frontend(PgSQL_Authentication *auth,
	const char *user, const char *pass,
	int default_hg = 0, int max_conn = 100)
{
	return auth->add(
		(char *)user, (char *)pass, USERNAME_FRONTEND,
		false,            // use_ssl
		default_hg,       // default_hostgroup
		false,            // transaction_persistent
		false,            // fast_forward
		max_conn,         // max_connections
		(char *)"",       // attributes
		(char *)""        // comment
	);
}

// ============================================================================
// 1. MySQL_Authentication: Core CRUD
// ============================================================================

/**
 * @brief Test basic add + exists + lookup cycle.
 */
static void test_mysql_add_exists_lookup() {
	mysql_add_frontend(GloMyAuth, "alice", "pass123", 1, 50);

	ok(GloMyAuth->exists((char *)"alice") == true,
		"MySQL: exists() returns true for added frontend user");

	ok(GloMyAuth->exists((char *)"unknown") == false,
		"MySQL: exists() returns false for nonexistent user");

	// Lookup with dup options
	dup_account_details_t dup = {true, true, true};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"alice", USERNAME_FRONTEND, dup);

	ok(ad.password != nullptr && strcmp(ad.password, "pass123") == 0,
		"MySQL: lookup() returns correct password");
	ok(ad.default_hostgroup == 1,
		"MySQL: lookup() returns correct default_hostgroup");
	ok(ad.max_connections == 50,
		"MySQL: lookup() returns correct max_connections");

	free_account_details(ad);
}

/**
 * @brief Test that exists() only checks frontends, not backends.
 */
static void test_mysql_exists_frontend_only() {
	mysql_add_backend(GloMyAuth, "backend_only", "secret");

	ok(GloMyAuth->exists((char *)"backend_only") == false,
		"MySQL: exists() returns false for backend-only user");

	// But lookup with USERNAME_BACKEND should find it
	dup_account_details_t dup = {false, false, false};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"backend_only", USERNAME_BACKEND, dup);

	ok(ad.password != nullptr && strcmp(ad.password, "secret") == 0,
		"MySQL: lookup(BACKEND) finds backend user");

	free_account_details(ad);
}

/**
 * @brief Test that add() overwrites on duplicate username.
 */
static void test_mysql_add_overwrites() {
	mysql_add_frontend(GloMyAuth, "bob", "old_pass", 1, 10);
	mysql_add_frontend(GloMyAuth, "bob", "new_pass", 2, 20);

	dup_account_details_t dup = {true, false, false};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"bob", USERNAME_FRONTEND, dup);

	ok(ad.password != nullptr && strcmp(ad.password, "new_pass") == 0,
		"MySQL: add() overwrites password on duplicate");
	ok(ad.default_hostgroup == 2,
		"MySQL: add() overwrites default_hostgroup on duplicate");
	ok(ad.max_connections == 20,
		"MySQL: add() overwrites max_connections on duplicate");

	free_account_details(ad);
}

/**
 * @brief Test del() removes a user.
 */
static void test_mysql_del() {
	mysql_add_frontend(GloMyAuth, "charlie", "pass");

	ok(GloMyAuth->exists((char *)"charlie") == true,
		"MySQL: user exists before del()");

	bool deleted = GloMyAuth->del((char *)"charlie", USERNAME_FRONTEND);
	ok(deleted == true, "MySQL: del() returns true for existing user");

	ok(GloMyAuth->exists((char *)"charlie") == false,
		"MySQL: user gone after del()");

	bool deleted_again = GloMyAuth->del((char *)"charlie", USERNAME_FRONTEND);
	ok(deleted_again == false,
		"MySQL: del() returns false for already-deleted user");
}

/**
 * @brief Test lookup() returns empty struct for nonexistent user.
 */
static void test_mysql_lookup_not_found() {
	dup_account_details_t dup = {true, true, true};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"nonexistent", USERNAME_FRONTEND, dup);

	ok(ad.password == nullptr,
		"MySQL: lookup() returns null password for nonexistent user");
	ok(ad.username == nullptr,
		"MySQL: lookup() returns null username for nonexistent user");
}

// ============================================================================
// 2. MySQL_Authentication: Credential Management
// ============================================================================

/**
 * @brief Test set_SHA1() stores and retrieves SHA1 hash.
 */
static void test_mysql_sha1() {
	mysql_add_frontend(GloMyAuth, "sha1user", "pass");

	unsigned char sha1_hash[SHA_DIGEST_LENGTH];
	SHA1((unsigned char *)"pass", 4, sha1_hash);

	bool set_ok = GloMyAuth->set_SHA1(
		(char *)"sha1user", USERNAME_FRONTEND, sha1_hash);
	ok(set_ok == true, "MySQL: set_SHA1() returns true");

	// Lookup with sha1 duplication
	dup_account_details_t dup = {false, true, false};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"sha1user", USERNAME_FRONTEND, dup);

	ok(ad.sha1_pass != nullptr, "MySQL: SHA1 pass retrieved via lookup()");
	if (ad.sha1_pass != nullptr) {
		ok(memcmp(ad.sha1_pass, sha1_hash, SHA_DIGEST_LENGTH) == 0,
			"MySQL: SHA1 hash matches what was set");
	} else {
		ok(0, "MySQL: SHA1 hash was unexpectedly null");
	}

	// set_SHA1 on nonexistent user
	bool set_fail = GloMyAuth->set_SHA1(
		(char *)"nobody", USERNAME_FRONTEND, sha1_hash);
	ok(set_fail == false,
		"MySQL: set_SHA1() returns false for nonexistent user");

	free_account_details(ad);
}

/**
 * @brief Test set_clear_text_password() for PRIMARY and ADDITIONAL,
 *        and verify stored values are retrievable via lookup().
 */
static void test_mysql_clear_text_password() {
	mysql_add_frontend(GloMyAuth, "ctpuser", "original");

	bool set_ok = GloMyAuth->set_clear_text_password(
		(char *)"ctpuser", USERNAME_FRONTEND,
		"clearpass", PASSWORD_TYPE::PRIMARY);
	ok(set_ok == true,
		"MySQL: set_clear_text_password(PRIMARY) returns true");

	set_ok = GloMyAuth->set_clear_text_password(
		(char *)"ctpuser", USERNAME_FRONTEND,
		"altpass", PASSWORD_TYPE::ADDITIONAL);
	ok(set_ok == true,
		"MySQL: set_clear_text_password(ADDITIONAL) returns true");

	// Verify stored clear-text passwords are retrievable
	dup_account_details_t dup = {false, false, false};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"ctpuser", USERNAME_FRONTEND, dup);
	ok(ad.clear_text_password[PASSWORD_TYPE::PRIMARY] != nullptr
		&& strcmp(ad.clear_text_password[PASSWORD_TYPE::PRIMARY], "clearpass") == 0,
		"MySQL: PRIMARY clear_text_password retrievable via lookup()");
	ok(ad.clear_text_password[PASSWORD_TYPE::ADDITIONAL] != nullptr
		&& strcmp(ad.clear_text_password[PASSWORD_TYPE::ADDITIONAL], "altpass") == 0,
		"MySQL: ADDITIONAL clear_text_password retrievable via lookup()");
	free_account_details(ad);

	bool set_fail = GloMyAuth->set_clear_text_password(
		(char *)"nobody", USERNAME_FRONTEND,
		"pass", PASSWORD_TYPE::PRIMARY);
	ok(set_fail == false,
		"MySQL: set_clear_text_password() returns false for nonexistent user");
}

// ============================================================================
// 3. MySQL_Authentication: Connection Counting
// ============================================================================

/**
 * @brief Test increase/decrease frontend user connections.
 */
static void test_mysql_connection_counting() {
	mysql_add_frontend(GloMyAuth, "connuser", "pass", 0, 3);

	// Increase connections until limit
	int remaining;
	remaining = GloMyAuth->increase_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);
	ok(remaining > 0, "MySQL: 1st connection: remaining > 0");

	remaining = GloMyAuth->increase_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);
	ok(remaining > 0, "MySQL: 2nd connection: remaining > 0");

	remaining = GloMyAuth->increase_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);
	ok(remaining > 0, "MySQL: 3rd connection: remaining > 0");

	// At limit now — next should return 0
	remaining = GloMyAuth->increase_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);
	ok(remaining == 0, "MySQL: 4th connection rejected (max_connections=3)");

	// Decrease and verify we can connect again
	GloMyAuth->decrease_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);

	remaining = GloMyAuth->increase_frontend_user_connections(
		(char *)"connuser", PASSWORD_TYPE::PRIMARY);
	ok(remaining > 0,
		"MySQL: connection allowed after decrease");
}

// ============================================================================
// 4. MySQL_Authentication: Bulk Operations
// ============================================================================

/**
 * @brief Test set_all_inactive + remove_inactives pattern.
 */
static void test_mysql_inactive_pattern() {
	// Start fresh
	GloMyAuth->reset();

	mysql_add_frontend(GloMyAuth, "keep_me", "pass1");
	mysql_add_frontend(GloMyAuth, "remove_me", "pass2");

	// Mark all inactive
	GloMyAuth->set_all_inactive(USERNAME_FRONTEND);

	// Re-add the one we want to keep (sets __active = true)
	mysql_add_frontend(GloMyAuth, "keep_me", "pass1");

	// Remove inactive users
	GloMyAuth->remove_inactives(USERNAME_FRONTEND);

	ok(GloMyAuth->exists((char *)"keep_me") == true,
		"MySQL: re-added user survives remove_inactives()");
	ok(GloMyAuth->exists((char *)"remove_me") == false,
		"MySQL: inactive user removed by remove_inactives()");
}

/**
 * @brief Test reset() clears all users.
 */
static void test_mysql_reset() {
	mysql_add_frontend(GloMyAuth, "user1", "p1");
	mysql_add_frontend(GloMyAuth, "user2", "p2");
	mysql_add_backend(GloMyAuth, "user3", "p3");

	GloMyAuth->reset();

	ok(GloMyAuth->exists((char *)"user1") == false,
		"MySQL: frontend user gone after reset()");

	dup_account_details_t dup = {false, false, false};
	account_details_t ad = GloMyAuth->lookup(
		(char *)"user3", USERNAME_BACKEND, dup);
	ok(ad.password == nullptr,
		"MySQL: backend user gone after reset()");
}

// ============================================================================
// 5. MySQL_Authentication: Checksums
// ============================================================================

/**
 * @brief Test runtime checksum behavior.
 */
static void test_mysql_checksums() {
	GloMyAuth->reset();

	uint64_t empty_checksum = GloMyAuth->get_runtime_checksum();
	ok(empty_checksum == 0,
		"MySQL: checksum is 0 with no users");

	mysql_add_frontend(GloMyAuth, "checksumA", "passA", 1);
	uint64_t checksum1 = GloMyAuth->get_runtime_checksum();
	ok(checksum1 != 0,
		"MySQL: checksum is non-zero with users");

	mysql_add_frontend(GloMyAuth, "checksumB", "passB", 2);
	uint64_t checksum2 = GloMyAuth->get_runtime_checksum();
	ok(checksum2 != checksum1,
		"MySQL: checksum changes when users are added");

	// Modify a user and verify checksum changes
	mysql_add_frontend(GloMyAuth, "checksumA", "passA_changed", 1);
	uint64_t checksum3 = GloMyAuth->get_runtime_checksum();
	ok(checksum3 != checksum2,
		"MySQL: checksum changes when password is modified");
}

// ============================================================================
// 6. MySQL_Authentication: Memory Usage
// ============================================================================

/**
 * @brief Test memory_usage() tracking.
 */
static void test_mysql_memory() {
	GloMyAuth->reset();

	unsigned int mem_empty = GloMyAuth->memory_usage();

	mysql_add_frontend(GloMyAuth, "memuser1", "password1");
	unsigned int mem_one = GloMyAuth->memory_usage();
	ok(mem_one > mem_empty,
		"MySQL: memory_usage() increases after add()");

	mysql_add_frontend(GloMyAuth, "memuser2", "password2_longer");
	unsigned int mem_two = GloMyAuth->memory_usage();
	ok(mem_two > mem_one,
		"MySQL: memory_usage() increases with more users");

	GloMyAuth->reset();
	unsigned int mem_after_reset = GloMyAuth->memory_usage();
	ok(mem_after_reset <= mem_empty,
		"MySQL: memory_usage() returns to baseline after reset()");
}

// ============================================================================
// 7. MySQL_Authentication: Frontend vs Backend Separation
// ============================================================================

/**
 * @brief Test that frontend and backend are independent.
 */
static void test_mysql_frontend_backend_separation() {
	GloMyAuth->reset();

	// Same username in both frontend and backend with different passwords
	mysql_add_frontend(GloMyAuth, "dualuser", "frontend_pass", 1);
	mysql_add_backend(GloMyAuth, "dualuser", "backend_pass", 2);

	dup_account_details_t dup = {false, false, false};

	account_details_t fe = GloMyAuth->lookup(
		(char *)"dualuser", USERNAME_FRONTEND, dup);
	ok(fe.password != nullptr && strcmp(fe.password, "frontend_pass") == 0,
		"MySQL: frontend lookup returns frontend password");
	ok(fe.default_hostgroup == 1,
		"MySQL: frontend lookup returns frontend hostgroup");

	account_details_t be = GloMyAuth->lookup(
		(char *)"dualuser", USERNAME_BACKEND, dup);
	ok(be.password != nullptr && strcmp(be.password, "backend_pass") == 0,
		"MySQL: backend lookup returns backend password");
	ok(be.default_hostgroup == 2,
		"MySQL: backend lookup returns backend hostgroup");

	free_account_details(fe);
	free_account_details(be);
}

// ============================================================================
// 8. PgSQL_Authentication: Core CRUD
// ============================================================================

/**
 * @brief Test PgSQL basic add + exists + lookup cycle.
 */
static void test_pgsql_add_exists_lookup() {
	pgsql_add_frontend(GloPgAuth, "pg_alice", "pgpass", 1, 50);

	ok(GloPgAuth->exists((char *)"pg_alice") == true,
		"PgSQL: exists() returns true for added frontend user");
	ok(GloPgAuth->exists((char *)"pg_unknown") == false,
		"PgSQL: exists() returns false for nonexistent user");

	// PgSQL lookup has different signature — returns password string
	bool use_ssl = false;
	int default_hg = -1, max_conn = -1;
	bool trans_persist = false, fast_fwd = false;
	void *sha1 = nullptr;
	char *attrs = nullptr;

	char *password = GloPgAuth->lookup(
		(char *)"pg_alice", USERNAME_FRONTEND,
		&use_ssl, &default_hg, &trans_persist, &fast_fwd,
		&max_conn, &sha1, &attrs);

	ok(password != nullptr && strcmp(password, "pgpass") == 0,
		"PgSQL: lookup() returns correct password");
	ok(default_hg == 1,
		"PgSQL: lookup() returns correct default_hostgroup");
	ok(max_conn == 50,
		"PgSQL: lookup() returns correct max_connections");

	if (password) free(password);
	if (attrs) free(attrs);
	if (sha1) free(sha1);
}

/**
 * @brief Test PgSQL del() and reset().
 */
static void test_pgsql_del_and_reset() {
	pgsql_add_frontend(GloPgAuth, "pg_del", "pass");
	ok(GloPgAuth->exists((char *)"pg_del") == true,
		"PgSQL: user exists before del()");

	bool del_ok = GloPgAuth->del((char *)"pg_del", USERNAME_FRONTEND);
	ok(del_ok == true, "PgSQL: del() returns true");
	ok(GloPgAuth->exists((char *)"pg_del") == false,
		"PgSQL: user gone after del()");

	// Test reset
	pgsql_add_frontend(GloPgAuth, "pg_r1", "p1");
	pgsql_add_frontend(GloPgAuth, "pg_r2", "p2");
	GloPgAuth->reset();
	ok(GloPgAuth->exists((char *)"pg_r1") == false,
		"PgSQL: user gone after reset()");
}

/**
 * @brief Test PgSQL connection counting.
 */
static void test_pgsql_connection_counting() {
	GloPgAuth->reset();
	pgsql_add_frontend(GloPgAuth, "pg_conn", "pass", 0, 2);

	int r1 = GloPgAuth->increase_frontend_user_connections(
		(char *)"pg_conn");
	ok(r1 > 0, "PgSQL: 1st connection allowed");

	int r2 = GloPgAuth->increase_frontend_user_connections(
		(char *)"pg_conn");
	ok(r2 > 0, "PgSQL: 2nd connection allowed");

	int r3 = GloPgAuth->increase_frontend_user_connections(
		(char *)"pg_conn");
	ok(r3 == 0, "PgSQL: 3rd connection rejected (max=2)");

	GloPgAuth->decrease_frontend_user_connections((char *)"pg_conn");
	int r4 = GloPgAuth->increase_frontend_user_connections(
		(char *)"pg_conn");
	ok(r4 > 0, "PgSQL: connection allowed after decrease");
}

/**
 * @brief Test PgSQL inactive pattern.
 */
static void test_pgsql_inactive_pattern() {
	GloPgAuth->reset();
	pgsql_add_frontend(GloPgAuth, "pg_keep", "p1");
	pgsql_add_frontend(GloPgAuth, "pg_drop", "p2");

	GloPgAuth->set_all_inactive(USERNAME_FRONTEND);
	pgsql_add_frontend(GloPgAuth, "pg_keep", "p1");
	GloPgAuth->remove_inactives(USERNAME_FRONTEND);

	ok(GloPgAuth->exists((char *)"pg_keep") == true,
		"PgSQL: re-added user survives remove_inactives()");
	ok(GloPgAuth->exists((char *)"pg_drop") == false,
		"PgSQL: inactive user removed");
}


// ============================================================================
// Main
// ============================================================================

int main() {
	plan(60);

	test_init_minimal();
	test_init_auth();

	// MySQL tests
	test_mysql_add_exists_lookup();          // 5 tests
	test_mysql_exists_frontend_only();       // 2 tests
	test_mysql_add_overwrites();             // 3 tests
	test_mysql_del();                        // 4 tests
	test_mysql_lookup_not_found();           // 2 tests
	test_mysql_sha1();                       // 4 tests
	test_mysql_clear_text_password();        // 3 tests
	test_mysql_connection_counting();        // 5 tests
	test_mysql_inactive_pattern();           // 2 tests
	test_mysql_reset();                      // 2 tests
	test_mysql_checksums();                  // 4 tests
	test_mysql_memory();                     // 3 tests
	test_mysql_frontend_backend_separation();// 4 tests

	// PgSQL tests
	test_pgsql_add_exists_lookup();          // 5 tests
	test_pgsql_del_and_reset();              // 4 tests (49-52)
	test_pgsql_connection_counting();        // 4 tests (53-56)
	test_pgsql_inactive_pattern();           // 2 tests (57-58)
	// Note: PgSQL does not have set_clear_text_password()
	// Note: PgSQL does not have default_schema or schema_locked

	test_cleanup_auth();
	test_cleanup_minimal();

	return exit_status();
}
