// Step 1 acceptance test for the genai plugin scaffold.
//
// Drives the actual genai .so through load → init → start → stop → unload,
// the same way proxysql will at startup.  Step 4.F extended the plugin
// to actually read mcp-* admin variables and the mcp_auth/target profile
// tables during start(), so this test now spins up real in-memory
// SQLite3DBs (instead of the previous fake `char*` stubs) and creates
// the tables genai_start expects.

#include "ProxySQL_PluginManager.h"
#include "sqlite3db.h"
#include "tap.h"

#include <string>

#ifndef PROXYSQL_GENAI_PLUGIN_PATH
#error "PROXYSQL_GENAI_PLUGIN_PATH must be defined"
#endif

namespace {

// Real in-memory SQLite databases.  Pre-populated with the table
// shapes genai_start() reads.  No data — that's enough to make the
// plugin's admin-DB queries succeed and return empty result sets,
// which is the "fresh-install" scenario.
SQLite3DB* g_admindb  = nullptr;
SQLite3DB* g_configdb = nullptr;
SQLite3DB* g_statsdb  = nullptr;

void setup_admindb_schema(SQLite3DB* db) {
	// Minimal schema to satisfy mcp_load_variables_from_admindb /
	// mcp_load_target_auth_map_from_admindb in the plugin.  Column
	// shapes mirror the canonical DDLs in
	// include/ProxySQL_Admin_Tables_Definitions.h so the plugin's
	// SELECTs (which name every column by name) succeed.
	db->execute("CREATE TABLE IF NOT EXISTS global_variables ("
	            " variable_name TEXT PRIMARY KEY, variable_value TEXT)");
	db->execute("CREATE TABLE IF NOT EXISTS mcp_auth_profiles ("
	            " auth_profile_id TEXT PRIMARY KEY, db_username TEXT,"
	            " db_password TEXT, default_schema TEXT DEFAULT '',"
	            " use_ssl INTEGER NOT NULL DEFAULT 0,"
	            " ssl_mode TEXT DEFAULT '',"
	            " comment TEXT DEFAULT '')");
	db->execute("CREATE TABLE IF NOT EXISTS mcp_target_profiles ("
	            " target_id TEXT PRIMARY KEY, protocol TEXT, hostgroup_id INTEGER,"
	            " auth_profile_id TEXT, description TEXT DEFAULT '',"
	            " max_rows INTEGER DEFAULT 200, timeout_ms INTEGER DEFAULT 2000,"
	            " allow_explain INTEGER DEFAULT 1, allow_discovery INTEGER DEFAULT 1,"
	            " active INTEGER DEFAULT 1, comment TEXT DEFAULT '')");
	db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_auth_profiles AS"
	            " SELECT * FROM mcp_auth_profiles WHERE 0");
	db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_target_profiles AS"
	            " SELECT * FROM mcp_target_profiles WHERE 0");
	db->execute("CREATE TABLE IF NOT EXISTS mcp_query_rules ("
	            " rule_id INTEGER PRIMARY KEY, active INTEGER NOT NULL DEFAULT 0,"
	            " username TEXT, target_id TEXT, schemaname TEXT, tool_name TEXT,"
	            " match_pattern TEXT, negate_match_pattern INTEGER NOT NULL DEFAULT 0,"
	            " re_modifiers TEXT, flagIN INTEGER NOT NULL DEFAULT 0, flagOUT INTEGER,"
	            " replace_pattern TEXT, timeout_ms INTEGER, error_msg TEXT, OK_msg TEXT,"
	            " log INTEGER, apply INTEGER NOT NULL DEFAULT 1, comment TEXT)");
	db->execute("CREATE TABLE IF NOT EXISTS runtime_mcp_query_rules AS"
	            " SELECT * FROM mcp_query_rules WHERE 0");
}

} // namespace

SQLite3DB* proxysql_plugin_get_admindb()  { return g_admindb; }
SQLite3DB* proxysql_plugin_get_configdb() { return g_configdb; }
SQLite3DB* proxysql_plugin_get_statsdb()  { return g_statsdb; }

int main() {
	plan(45);

	g_admindb  = new SQLite3DB();
	g_configdb = new SQLite3DB();
	g_statsdb  = new SQLite3DB();
	g_admindb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
	g_configdb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
	g_statsdb->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
	setup_admindb_schema(g_admindb);

	ProxySQL_PluginManager mgr;
	std::string err {};

	const bool loaded = mgr.load(PROXYSQL_GENAI_PLUGIN_PATH, err);
	ok(loaded, "load genai plugin succeeds");
	if (!loaded) {
		diag("load error: %s", err.c_str());
		BAIL_OUT("genai plugin must load before lifecycle assertions");
	}
	ok(mgr.size() == 1, "exactly one plugin handle after load");

	// Phase B (chassis): every plugin's register_schemas callback runs
	// here, BEFORE init_all.  As of Step 4.G the genai plugin uses this
	// callback to publish its admin/config/stats table set, so it MUST
	// fire before the size assertions below or we'll see 0 tables in
	// every kind.
	ok(mgr.invoke_register_schemas_phase(err),
	   "invoke_register_schemas_phase succeeds");
	if (!err.empty()) diag("register_schemas error: %s", err.c_str());

	ok(mgr.init_all(err),  "init_all succeeds");
	if (!err.empty()) diag("init error: %s", err.c_str());

	ok(mgr.start_all(err), "start_all succeeds");
	if (!err.empty()) diag("start error: %s", err.c_str());

	// Runtime-view dispatch: SELECT against runtime_mcp_<X> should
	// trigger the chassis dispatcher to invoke the plugin's
	// project_*_to_runtime_view callback before the SELECT runs.
	// Seed the editable mcp_<X> tables, drive `LOAD MCP PROFILES TO
	// RUNTIME` through the plugin command registry, then assert the
	// runtime_<X> tables match.  This is the end-to-end coverage that
	// plugin_runtime_views_unit-t can't provide because that test uses
	// synthetic callbacks.
	ok(g_admindb->execute(
		"INSERT INTO mcp_auth_profiles"
		" (auth_profile_id, db_username, db_password, default_schema,"
		"  use_ssl, ssl_mode, comment)"
		" VALUES('a','u','p','',0,'','')") ,
	   "seed mcp_auth_profiles");
	ok(g_admindb->execute(
		"INSERT INTO mcp_target_profiles"
		" (target_id, protocol, hostgroup_id, auth_profile_id, description,"
		"  max_rows, timeout_ms, allow_explain, allow_discovery, active, comment)"
		" VALUES('t','mysql',1,'a','',200,2000,1,1,1,'')") ,
	   "seed mcp_target_profiles");

	ProxySQL_PluginCommandContext cmd_ctx { g_admindb, g_configdb, g_statsdb };
	ProxySQL_PluginCommandResult cmd_result;
	const bool dispatched = mgr.dispatch_admin_command(
		cmd_ctx, "LOAD MCP PROFILES TO RUNTIME", cmd_result);
	ok(dispatched && cmd_result.error_code == 0,
	   "LOAD MCP PROFILES TO RUNTIME dispatches via plugin (rc=%d, msg=%s)",
	   cmd_result.error_code, cmd_result.message.c_str());

	// The chassis pre-SELECT hook should have refreshed runtime_<X>
	// from the plugin's snapshot.  In a real Admin SQL flow that hook
	// fires during GenericRefreshStatistics; the unit test invokes it
	// explicitly because we don't have a full Admin module wired up.
	mgr.refresh_runtime_views_for_query(
		"SELECT * FROM runtime_mcp_auth_profiles", g_admindb, nullptr, nullptr);
	mgr.refresh_runtime_views_for_query(
		"SELECT * FROM runtime_mcp_target_profiles", g_admindb, nullptr, nullptr);

	ok(g_admindb->return_one_int(
		"SELECT COUNT(*) FROM runtime_mcp_auth_profiles") == 1,
	   "runtime_mcp_auth_profiles populated by project callback");
	ok(g_admindb->return_one_int(
		"SELECT COUNT(*) FROM runtime_mcp_target_profiles") == 1,
	   "runtime_mcp_target_profiles populated by project callback");

	// SAVE round-trip: edit main.* directly (operator typo), then
	// dispatch SAVE MCP PROFILES TO MEMORY and verify the in-memory
	// snapshot was written back over the operator's edit.  This
	// closes the loop on the install/save/project triplet — without
	// it, install + project would be tested but SAVE would not.
	ok(g_admindb->execute(
		"UPDATE mcp_target_profiles SET hostgroup_id=999 WHERE target_id='t'"),
	   "operator stomps target hostgroup_id=999 in main");
	ok(g_admindb->return_one_int(
		"SELECT hostgroup_id FROM mcp_target_profiles WHERE target_id='t'") == 999,
	   "main reflects operator stomp");

	ProxySQL_PluginCommandResult save_result;
	const bool save_dispatched = mgr.dispatch_admin_command(
		cmd_ctx, "SAVE MCP PROFILES TO MEMORY", save_result);
	ok(save_dispatched && save_result.error_code == 0,
	   "SAVE MCP PROFILES TO MEMORY dispatches via plugin (rc=%d, msg=%s)",
	   save_result.error_code, save_result.message.c_str());
	ok(g_admindb->return_one_int(
		"SELECT hostgroup_id FROM mcp_target_profiles WHERE target_id='t'") == 1,
	   "SAVE restored hostgroup_id=1 from in-memory snapshot");

	ok(mgr.stop_all(),     "stop_all succeeds");

	// Verify the plugin handles are still live (pre-destructor).
	ok(mgr.size() == 1, "plugin handle still present after stop_all");

	// Step 4.G: the plugin now owns the MCP admin / config table set.
	// Counts match plugins/genai/src/plugin_tables.cpp.
	//   admin:  6 (mcp_query_rules, mcp_auth_profiles, mcp_target_profiles
	//              + their runtime_* siblings)
	//   config: 3 (the persisted-only variants — no runtime_*)
	ok(mgr.tables(ProxySQL_PluginDBKind::admin_db).size() == 6,
	   "genai plugin registers 6 admin-db tables (got %zu)",
	   mgr.tables(ProxySQL_PluginDBKind::admin_db).size());
	ok(mgr.tables(ProxySQL_PluginDBKind::config_db).size() == 3,
	   "genai plugin registers 3 config-db tables (got %zu)",
	   mgr.tables(ProxySQL_PluginDBKind::config_db).size());

	// Step 4.F: the plugin registers MCP admin SQL verbs.  Verify
	// each registered alias resolves back to the canonical command
	// via the plugin manager's alias resolver (which is the same
	// path admin SQL dispatch uses).
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES TO RUNTIME") ==
	       "LOAD MCP VARIABLES TO RUNTIME",
	   "canonical: LOAD MCP VARIABLES TO RUNTIME registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM MEMORY") ==
	       "LOAD MCP VARIABLES TO RUNTIME",
	   "alias: LOAD MCP VARIABLES FROM MEMORY -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM MEM") ==
	       "LOAD MCP VARIABLES TO RUNTIME",
	   "alias: LOAD MCP VARIABLES FROM MEM -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM DISK") ==
	       "LOAD MCP VARIABLES FROM DISK",
	   "canonical: LOAD MCP VARIABLES FROM DISK registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES TO MEMORY") ==
	       "LOAD MCP VARIABLES FROM DISK",
	   "alias: LOAD MCP VARIABLES TO MEMORY -> FROM DISK");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP VARIABLES FROM CONFIG") ==
	       "LOAD MCP VARIABLES FROM CONFIG",
	   "canonical: LOAD MCP VARIABLES FROM CONFIG registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO RUNTIME") ==
	       "LOAD MCP PROFILES TO RUNTIME",
	   "canonical: LOAD MCP PROFILES TO RUNTIME registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES FROM MEMORY") ==
	       "LOAD MCP PROFILES TO RUNTIME",
	   "alias: LOAD MCP PROFILES FROM MEMORY -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO RUN") ==
	       "LOAD MCP PROFILES TO RUNTIME",
	   "alias: LOAD MCP PROFILES TO RUN -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES FROM DISK") ==
	       "LOAD MCP PROFILES FROM DISK",
	   "canonical: LOAD MCP PROFILES FROM DISK registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP PROFILES TO MEMORY") ==
	       "LOAD MCP PROFILES FROM DISK",
	   "alias: LOAD MCP PROFILES TO MEMORY -> FROM DISK");

	// SAVE direction: pull runtime mcp-* values back into main.
	ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO MEMORY") ==
	       "SAVE MCP VARIABLES TO MEMORY",
	   "canonical: SAVE MCP VARIABLES TO MEMORY registered");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES FROM RUNTIME") ==
	       "SAVE MCP VARIABLES TO MEMORY",
	   "alias: SAVE MCP VARIABLES FROM RUNTIME -> TO MEMORY");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES FROM RUN") ==
	       "SAVE MCP VARIABLES TO MEMORY",
	   "alias: SAVE MCP VARIABLES FROM RUN -> TO MEMORY");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO MEM") ==
	       "SAVE MCP VARIABLES TO MEMORY",
	   "alias: SAVE MCP VARIABLES TO MEM -> TO MEMORY");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP VARIABLES TO DISK") ==
	       "SAVE MCP VARIABLES TO DISK",
	   "canonical: SAVE MCP VARIABLES TO DISK registered");

	// MCP QUERY RULES verbs.
	ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES TO RUNTIME") ==
	       "LOAD MCP QUERY RULES TO RUNTIME",
	   "canonical: LOAD MCP QUERY RULES TO RUNTIME registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM MEMORY") ==
	       "LOAD MCP QUERY RULES TO RUNTIME",
	   "alias: LOAD MCP QUERY RULES FROM MEMORY -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM MEM") ==
	       "LOAD MCP QUERY RULES TO RUNTIME",
	   "alias: LOAD MCP QUERY RULES FROM MEM -> TO RUNTIME");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES TO MEMORY") ==
	       "SAVE MCP QUERY RULES TO MEMORY",
	   "canonical: SAVE MCP QUERY RULES TO MEMORY registered");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES FROM RUNTIME") ==
	       "SAVE MCP QUERY RULES TO MEMORY",
	   "alias: SAVE MCP QUERY RULES FROM RUNTIME -> TO MEMORY");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES FROM RUN") ==
	       "SAVE MCP QUERY RULES TO MEMORY",
	   "alias: SAVE MCP QUERY RULES FROM RUN -> TO MEMORY");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES FROM DISK") ==
	       "LOAD MCP QUERY RULES FROM DISK",
	   "canonical: LOAD MCP QUERY RULES FROM DISK registered");
	ok(mgr.resolve_alias_to_canonical("LOAD MCP QUERY RULES TO MEMORY") ==
	       "LOAD MCP QUERY RULES FROM DISK",
	   "alias: LOAD MCP QUERY RULES TO MEMORY -> FROM DISK");
	ok(mgr.resolve_alias_to_canonical("SAVE MCP QUERY RULES TO DISK") ==
	       "SAVE MCP QUERY RULES TO DISK",
	   "canonical: SAVE MCP QUERY RULES TO DISK registered");
	ok(mgr.resolve_alias_to_canonical("LOAD GENAI VARIABLES FROM CONFIG") ==
	       "LOAD GENAI VARIABLES FROM CONFIG",
	   "canonical: LOAD GENAI VARIABLES FROM CONFIG registered");

	// Sanity: an unrelated verb does NOT resolve via the plugin.
	ok(mgr.resolve_alias_to_canonical("SELECT 1").empty(),
	   "unknown SQL does not resolve via plugin command registry");

	delete g_admindb;
	delete g_configdb;
	delete g_statsdb;

	return exit_status();
}
