/**
 * @file statistics_unit-t.cpp
 * @brief Unit tests for ProxySQL_Statistics.
 *
 * Tests cover:
 *   - Constructor / init / table creation
 *   - Timer functions (MySQL_Threads_Handler_timetoget, system_cpu_timetoget,
 *     MySQL_Query_Cache_timetoget, eventslog timer functions)
 *   - TSDB variable management (set_variable, get_variable, has_variable,
 *     get_variables_list) — guarded by PROXYSQLTSDB
 *   - TSDB timer functions (tsdb_sampler_timetoget, tsdb_downsample_timetoget,
 *     tsdb_monitor_timetoget, tsdb_retention_timetoget) — guarded by PROXYSQLTSDB
 *   - TSDB metric insertion and query (insert_tsdb_metric, query_tsdb_metrics)
 *     which indirectly exercises the anonymous-namespace helpers
 *     escape_sql_string_literal and valid_label_key
 *   - TSDB backend health insertion and query
 *   - TSDB downsampling and retention cleanup
 *   - TSDB status retrieval
 *
 * All tests use in-memory or /tmp SQLite3 databases — no external deps.
 */

#include "tap.h"
#include "test_globals.h"
#include "test_init.h"
#include "proxysql.h"
#include "cpp.h"
#include "ProxySQL_Statistics.hpp"

#include <cstring>
#include <cstdlib>
#include <string>
#include <map>
#include <sys/stat.h>
#include <unistd.h>

// Forward-declare the extern that the constructor reads.
// GloVars is defined in test_globals.cpp via the PROXYSQL_EXTERN mechanism.

static ProxySQL_Statistics *stats = nullptr;
static std::string tmpdb_path;

// ============================================================
// Setup / teardown
// ============================================================

static void setup_stats() {
	// ProxySQL_Statistics constructor opens GloVars.statsdb_disk as a file path.
	// Use /tmp directly to avoid datadir issues in CI environments.
	const char *tmpdir = getenv("TMPDIR");
	if (tmpdir == nullptr) {
		tmpdir = "/tmp";
	}
	tmpdb_path = std::string(tmpdir) + "/proxysql_test_statsdb_" + std::to_string(getpid()) + ".db";

	// Ensure datadir exists (needed by other parts of ProxySQL_Statistics)
	if (GloVars.datadir != nullptr) {
		mkdir(GloVars.datadir, 0755);
	}

	// Set the global so the constructor can open it
	GloVars.statsdb_disk = strdup(tmpdb_path.c_str());

	stats = new ProxySQL_Statistics();
	if (stats->statsdb_disk == nullptr) {
		diag("statsdb_disk is NULL after construction — possible struct layout mismatch");
		diag("(rebuild with: make clean && make debug in test/tap/tests/unit/)");
		diag("GloVars.statsdb_disk=%s", GloVars.statsdb_disk ? GloVars.statsdb_disk : "NULL");
	}
	if (stats->statsdb_disk != nullptr) {
		stats->init();
	}
}

static void teardown_stats() {
	delete stats;
	stats = nullptr;

	// Clean up temp database files
	unlink(tmpdb_path.c_str());
	std::string wal = tmpdb_path + "-wal";
	std::string shm = tmpdb_path + "-shm";
	unlink(wal.c_str());
	unlink(shm.c_str());

	if (GloVars.statsdb_disk != nullptr) {
		free(GloVars.statsdb_disk);
		GloVars.statsdb_disk = nullptr;
	}
}

// ============================================================
// Constructor and init
// ============================================================

static bool statsdb_ok = false;

static void test_constructor_creates_databases() {
	ok(stats != nullptr, "ProxySQL_Statistics object created successfully");
	statsdb_ok = (stats != nullptr && stats->statsdb_disk != nullptr);
	ok(statsdb_ok, "statsdb_disk is not null after construction");
}

static void test_init_creates_tables() {
	if (!statsdb_ok) {
		skip(3, "statsdb_disk is NULL — struct layout mismatch (clean rebuild needed)");
		return;
	}
	// After init(), standard tables should exist. Verify by querying them.
	SQLite3_result *resultset = nullptr;
	char *error = nullptr;
	int cols = 0, affected_rows = 0;

	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='mysql_connections'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: mysql_connections table exists on disk");
	delete resultset;

	resultset = nullptr;
	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='system_cpu'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: system_cpu table exists on disk");
	delete resultset;

	resultset = nullptr;
	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='history_mysql_query_digest'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: history_mysql_query_digest table exists on disk");
	delete resultset;
}

#ifdef PROXYSQLTSDB
static void test_init_creates_tsdb_tables() {
	SQLite3_result *resultset = nullptr;
	char *error = nullptr;
	int cols = 0, affected_rows = 0;

	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: tsdb_metrics table exists on disk");
	delete resultset;

	resultset = nullptr;
	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics_hour'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: tsdb_metrics_hour table exists on disk");
	delete resultset;

	resultset = nullptr;
	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_backend_health'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	ok(resultset != nullptr && resultset->rows_count > 0 &&
	   strcmp(resultset->rows[0]->fields[0], "1") == 0,
	   "init: tsdb_backend_health table exists on disk");
	delete resultset;
}
#endif

// ============================================================
// Timer functions — MySQL_Threads_Handler_timetoget
// ============================================================

static void test_timer_returns_false_when_disabled() {
	// When stats_mysql_connections is 0, timetoget should return false
	stats->variables.stats_mysql_connections = 0;
	bool result = stats->MySQL_Threads_Handler_timetoget(1000000ULL);
	ok(result == false, "MySQL_Threads_Handler_timetoget: returns false when interval is 0");
}

static void test_timer_returns_true_on_first_call() {
	// With interval > 0 and curtime > next_timer (initially 0), should return true
	stats->variables.stats_mysql_connections = 5;
	bool result = stats->MySQL_Threads_Handler_timetoget(5000000ULL);
	ok(result == true, "MySQL_Threads_Handler_timetoget: returns true on first call (timer was 0)");
}

static void test_timer_returns_false_before_interval() {
	// After a successful trigger, calling again immediately should return false
	stats->variables.stats_mysql_connections = 60;  // 60 second interval
	// First call triggers and sets next_timer
	stats->MySQL_Threads_Handler_timetoget(1000000000ULL);  // 1000 seconds in usec
	// Call again at almost the same time — should not trigger
	bool result = stats->MySQL_Threads_Handler_timetoget(1000000001ULL);
	ok(result == false, "MySQL_Threads_Handler_timetoget: returns false before interval elapses");
}

static void test_timer_returns_true_after_interval() {
	stats->variables.stats_mysql_connections = 5;  // 5 second interval
	// First call at t=1000s triggers
	stats->MySQL_Threads_Handler_timetoget(1000000000ULL);
	// Call again at t=1006s — should trigger (5s+ elapsed)
	bool result = stats->MySQL_Threads_Handler_timetoget(1006000000ULL);
	ok(result == true, "MySQL_Threads_Handler_timetoget: returns true after interval elapses");
}

// ============================================================
// Timer functions — MySQL_Query_Cache_timetoget
// ============================================================

static void test_query_cache_timer_disabled() {
	stats->variables.stats_mysql_query_cache = 0;
	ok(stats->MySQL_Query_Cache_timetoget(5000000ULL) == false,
	   "MySQL_Query_Cache_timetoget: returns false when interval is 0");
}

static void test_query_cache_timer_triggers() {
	stats->variables.stats_mysql_query_cache = 10;
	ok(stats->MySQL_Query_Cache_timetoget(2000000000ULL) == true,
	   "MySQL_Query_Cache_timetoget: returns true on first call");
}

// ============================================================
// Timer functions — system_cpu_timetoget
// ============================================================

static void test_system_cpu_timer_disabled() {
	stats->variables.stats_system_cpu = 0;
	ok(stats->system_cpu_timetoget(5000000ULL) == false,
	   "system_cpu_timetoget: returns false when interval is 0");
}

static void test_system_cpu_timer_triggers() {
	stats->variables.stats_system_cpu = 10;
	ok(stats->system_cpu_timetoget(3000000000ULL) == true,
	   "system_cpu_timetoget: returns true on first call");
}

// ============================================================
// Timer functions — eventslog timers
// ============================================================

static void test_mysql_eventslog_timer_disabled() {
	stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 0;
	ok(stats->MySQL_Logger_dump_eventslog_timetoget(5000000ULL) == false,
	   "MySQL_Logger_dump_eventslog_timetoget: returns false when interval is 0");
}

static void test_mysql_eventslog_timer_triggers() {
	stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 5;
	ok(stats->MySQL_Logger_dump_eventslog_timetoget(6000000ULL) == true,
	   "MySQL_Logger_dump_eventslog_timetoget: returns true on first call");
}

static void test_mysql_eventslog_timer_no_retrigger() {
	stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 10;
	stats->MySQL_Logger_dump_eventslog_timetoget(10000000ULL);  // triggers, sets last_timer
	ok(stats->MySQL_Logger_dump_eventslog_timetoget(15000000ULL) == false,
	   "MySQL_Logger_dump_eventslog_timetoget: returns false before 10s elapses");
}

static void test_pgsql_eventslog_timer_disabled() {
	stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 0;
	ok(stats->PgSQL_Logger_dump_eventslog_timetoget(5000000ULL) == false,
	   "PgSQL_Logger_dump_eventslog_timetoget: returns false when interval is 0");
}

static void test_pgsql_eventslog_timer_triggers() {
	stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 5;
	ok(stats->PgSQL_Logger_dump_eventslog_timetoget(6000000ULL) == true,
	   "PgSQL_Logger_dump_eventslog_timetoget: returns true on first call");
}

// ============================================================
// Timer functions — mysql_query_digest_to_disk_timetoget
// ============================================================

static void test_digest_to_disk_timer_disabled() {
	stats->variables.stats_mysql_query_digest_to_disk = 0;
	ok(stats->mysql_query_digest_to_disk_timetoget(5000000ULL) == false,
	   "mysql_query_digest_to_disk_timetoget: returns false when interval is 0");
}

static void test_digest_to_disk_timer_triggers() {
	stats->variables.stats_mysql_query_digest_to_disk = 10;
	ok(stats->mysql_query_digest_to_disk_timetoget(4000000000ULL) == true,
	   "mysql_query_digest_to_disk_timetoget: returns true on first call");
}

// ============================================================
// TSDB variable management (PROXYSQLTSDB only)
// ============================================================

#ifdef PROXYSQLTSDB

static void test_set_variable_enabled() {
	ok(stats->set_variable("enabled", "1") == true,
	   "set_variable: 'enabled' accepts value '1'");
	ok(stats->set_variable("enabled", "0") == true,
	   "set_variable: 'enabled' accepts value '0'");
}

static void test_set_variable_enabled_out_of_range() {
	ok(stats->set_variable("enabled", "2") == false,
	   "set_variable: 'enabled' rejects value '2' (max=1)");
	ok(stats->set_variable("enabled", "-1") == false,
	   "set_variable: 'enabled' rejects value '-1' (min=0)");
}

static void test_set_variable_sample_interval() {
	ok(stats->set_variable("sample_interval", "1") == true,
	   "set_variable: 'sample_interval' accepts min value '1'");
	ok(stats->set_variable("sample_interval", "3600") == true,
	   "set_variable: 'sample_interval' accepts max value '3600'");
	ok(stats->set_variable("sample_interval", "0") == false,
	   "set_variable: 'sample_interval' rejects value '0' (below min)");
	ok(stats->set_variable("sample_interval", "3601") == false,
	   "set_variable: 'sample_interval' rejects value '3601' (above max)");
}

static void test_set_variable_retention_days() {
	ok(stats->set_variable("retention_days", "1") == true,
	   "set_variable: 'retention_days' accepts value '1'");
	ok(stats->set_variable("retention_days", "3650") == true,
	   "set_variable: 'retention_days' accepts max value '3650'");
	ok(stats->set_variable("retention_days", "0") == false,
	   "set_variable: 'retention_days' rejects '0'");
	ok(stats->set_variable("retention_days", "3651") == false,
	   "set_variable: 'retention_days' rejects '3651'");
}

static void test_set_variable_monitor_enabled() {
	ok(stats->set_variable("monitor_enabled", "0") == true,
	   "set_variable: 'monitor_enabled' accepts '0'");
	ok(stats->set_variable("monitor_enabled", "1") == true,
	   "set_variable: 'monitor_enabled' accepts '1'");
	ok(stats->set_variable("monitor_enabled", "3") == false,
	   "set_variable: 'monitor_enabled' rejects '3'");
}

static void test_set_variable_monitor_interval() {
	ok(stats->set_variable("monitor_interval", "10") == true,
	   "set_variable: 'monitor_interval' accepts '10'");
	ok(stats->set_variable("monitor_interval", "0") == false,
	   "set_variable: 'monitor_interval' rejects '0'");
}

static void test_set_variable_invalid_name() {
	ok(stats->set_variable("nonexistent_var", "1") == false,
	   "set_variable: unknown variable name returns false");
}

static void test_set_variable_null_inputs() {
	ok(stats->set_variable(nullptr, "1") == false,
	   "set_variable: null name returns false");
	ok(stats->set_variable("enabled", nullptr) == false,
	   "set_variable: null value returns false");
	ok(stats->set_variable("enabled", "") == false,
	   "set_variable: empty value returns false");
}

static void test_set_variable_non_integer_value() {
	ok(stats->set_variable("enabled", "abc") == false,
	   "set_variable: non-integer value returns false");
	ok(stats->set_variable("enabled", "1.5") == false,
	   "set_variable: float value returns false");
	ok(stats->set_variable("enabled", "1abc") == false,
	   "set_variable: trailing garbage returns false");
}

static void test_set_variable_case_insensitive() {
	ok(stats->set_variable("ENABLED", "1") == true,
	   "set_variable: variable name is case-insensitive (uppercase)");
	ok(stats->set_variable("Enabled", "0") == true,
	   "set_variable: variable name is case-insensitive (mixed case)");
}

static void test_get_variable() {
	stats->set_variable("enabled", "1");
	char *val = stats->get_variable("enabled");
	ok(val != nullptr && strcmp(val, "1") == 0,
	   "get_variable: returns '1' after set_variable('enabled', '1')");
	free(val);

	stats->set_variable("sample_interval", "42");
	val = stats->get_variable("sample_interval");
	ok(val != nullptr && strcmp(val, "42") == 0,
	   "get_variable: returns '42' after set_variable('sample_interval', '42')");
	free(val);

	stats->set_variable("retention_days", "365");
	val = stats->get_variable("retention_days");
	ok(val != nullptr && strcmp(val, "365") == 0,
	   "get_variable: returns '365' after set_variable('retention_days', '365')");
	free(val);

	stats->set_variable("monitor_enabled", "1");
	val = stats->get_variable("monitor_enabled");
	ok(val != nullptr && strcmp(val, "1") == 0,
	   "get_variable: returns '1' for monitor_enabled");
	free(val);

	stats->set_variable("monitor_interval", "30");
	val = stats->get_variable("monitor_interval");
	ok(val != nullptr && strcmp(val, "30") == 0,
	   "get_variable: returns '30' for monitor_interval");
	free(val);
}

static void test_get_variable_null_and_unknown() {
	ok(stats->get_variable(nullptr) == nullptr,
	   "get_variable: null name returns null");
	ok(stats->get_variable("nonexistent") == nullptr,
	   "get_variable: unknown name returns null");
}

static void test_has_variable() {
	ok(stats->has_variable("enabled") == true,
	   "has_variable: 'enabled' exists");
	ok(stats->has_variable("sample_interval") == true,
	   "has_variable: 'sample_interval' exists");
	ok(stats->has_variable("retention_days") == true,
	   "has_variable: 'retention_days' exists");
	ok(stats->has_variable("monitor_enabled") == true,
	   "has_variable: 'monitor_enabled' exists");
	ok(stats->has_variable("monitor_interval") == true,
	   "has_variable: 'monitor_interval' exists");
	ok(stats->has_variable("nonexistent") == false,
	   "has_variable: 'nonexistent' does not exist");
	ok(stats->has_variable(nullptr) == false,
	   "has_variable: null returns false");
}

static void test_get_variables_list() {
	char **list = stats->get_variables_list();
	ok(list != nullptr, "get_variables_list: returns non-null");

	int count = 0;
	if (list) {
		while (list[count] != nullptr) count++;
	}
	ok(count == 5, "get_variables_list: returns 5 variables (got %d)", count);

	// Free the list
	if (list) {
		for (int i = 0; list[i]; i++) free(list[i]);
		free(list);
	}
}

// ============================================================
// TSDB timer functions
// ============================================================

static void test_tsdb_sampler_timer_disabled() {
	stats->set_variable("enabled", "0");
	ok(stats->tsdb_sampler_timetoget(5000000ULL) == false,
	   "tsdb_sampler_timetoget: returns false when TSDB disabled");
}

static void test_tsdb_sampler_timer_triggers() {
	stats->set_variable("enabled", "1");
	stats->set_variable("sample_interval", "5");
	// next_timer starts at 0, curtime > 0 should trigger
	ok(stats->tsdb_sampler_timetoget(1000000ULL) == true,
	   "tsdb_sampler_timetoget: returns true on first call when enabled");
}

static void test_tsdb_sampler_timer_no_retrigger() {
	stats->set_variable("enabled", "1");
	stats->set_variable("sample_interval", "10");
	stats->tsdb_sampler_timetoget(1000000ULL);  // triggers, sets next = 1 + 10s = 11000000
	ok(stats->tsdb_sampler_timetoget(5000000ULL) == false,
	   "tsdb_sampler_timetoget: returns false before interval elapses");
}

static void test_tsdb_downsample_timer() {
	stats->set_variable("enabled", "0");
	ok(stats->tsdb_downsample_timetoget(5000000ULL) == false,
	   "tsdb_downsample_timetoget: returns false when disabled");

	stats->set_variable("enabled", "1");
	ok(stats->tsdb_downsample_timetoget(1000000ULL) == true,
	   "tsdb_downsample_timetoget: returns true on first call when enabled");
}

static void test_tsdb_monitor_timer() {
	stats->set_variable("enabled", "1");
	stats->set_variable("monitor_enabled", "0");
	ok(stats->tsdb_monitor_timetoget(5000000ULL) == false,
	   "tsdb_monitor_timetoget: returns false when monitor disabled");

	stats->set_variable("monitor_enabled", "1");
	stats->set_variable("monitor_interval", "10");
	ok(stats->tsdb_monitor_timetoget(1000000ULL) == true,
	   "tsdb_monitor_timetoget: returns true on first call when both enabled");
}

static void test_tsdb_retention_timer() {
	stats->set_variable("enabled", "0");
	ok(stats->tsdb_retention_timetoget(5000000ULL) == false,
	   "tsdb_retention_timetoget: returns false when disabled");

	stats->set_variable("enabled", "1");
	ok(stats->tsdb_retention_timetoget(1000000ULL) == true,
	   "tsdb_retention_timetoget: returns true on first call when enabled");
}

// ============================================================
// TSDB metric insertion and query
// ============================================================

static void test_insert_and_query_tsdb_metric() {
	stats->set_variable("enabled", "1");

	std::map<std::string, std::string> labels;
	labels["instance"] = "localhost:6032";

	time_t now = time(nullptr);
	stats->insert_tsdb_metric("test_metric", labels, 42.5, now);
	stats->insert_tsdb_metric("test_metric", labels, 43.0, now + 1);

	// Query back
	std::map<std::string, std::string> no_filter;
	SQLite3_result *result = stats->query_tsdb_metrics("test_metric", no_filter, now - 10, now + 10);
	ok(result != nullptr, "query_tsdb_metrics: returns non-null result for inserted metric");
	if (result) {
		ok(result->rows_count == 2, "query_tsdb_metrics: returns 2 rows (got %lu)",
		   (unsigned long)result->rows_count);
		delete result;
	} else {
		ok(false, "query_tsdb_metrics: returns 2 rows (null result)");
	}
}

static void test_query_tsdb_metric_with_label_filter() {
	time_t now = time(nullptr) + 100;  // offset to not collide with previous test

	std::map<std::string, std::string> labels_a;
	labels_a["host"] = "server_a";
	stats->insert_tsdb_metric("cpu_usage", labels_a, 75.0, now);

	std::map<std::string, std::string> labels_b;
	labels_b["host"] = "server_b";
	stats->insert_tsdb_metric("cpu_usage", labels_b, 90.0, now);

	// Filter by label
	std::map<std::string, std::string> filter;
	filter["host"] = "server_a";
	SQLite3_result *result = stats->query_tsdb_metrics("cpu_usage", filter, now - 10, now + 10);
	ok(result != nullptr, "query_tsdb_metrics: label filter returns non-null");
	if (result) {
		ok(result->rows_count == 1, "query_tsdb_metrics: label filter returns 1 row (got %lu)",
		   (unsigned long)result->rows_count);
		delete result;
	} else {
		ok(false, "query_tsdb_metrics: label filter returns 1 row (null result)");
	}
}

static void test_query_tsdb_metric_with_single_quote_in_name() {
	// This exercises escape_sql_string_literal indirectly
	time_t now = time(nullptr) + 200;

	std::map<std::string, std::string> labels;
	stats->insert_tsdb_metric("metric_with'quote", labels, 10.0, now);

	std::map<std::string, std::string> no_filter;
	SQLite3_result *result = stats->query_tsdb_metrics("metric_with'quote", no_filter, now - 10, now + 10);
	ok(result != nullptr, "query_tsdb_metrics: metric name with single quote works (escape_sql_string_literal)");
	if (result) {
		ok(result->rows_count == 1, "query_tsdb_metrics: single-quote metric returns 1 row (got %lu)",
		   (unsigned long)result->rows_count);
		delete result;
	} else {
		ok(false, "query_tsdb_metrics: single-quote metric returns 1 row (null result)");
	}
}

static void test_query_tsdb_metric_swapped_time_range() {
	// query_tsdb_metrics should swap from/to if to < from
	time_t now = time(nullptr) + 300;
	std::map<std::string, std::string> labels;
	stats->insert_tsdb_metric("swap_test", labels, 1.0, now);

	std::map<std::string, std::string> no_filter;
	SQLite3_result *result = stats->query_tsdb_metrics("swap_test", no_filter, now + 10, now - 10);
	ok(result != nullptr, "query_tsdb_metrics: swapped time range still returns result");
	if (result) {
		ok(result->rows_count == 1, "query_tsdb_metrics: swapped range returns correct row count");
		delete result;
	} else {
		ok(false, "query_tsdb_metrics: swapped range returns correct row count (null)");
	}
}

static void test_query_tsdb_metric_invalid_label_key() {
	// valid_label_key rejects keys with special chars — query should return NULL
	time_t now = time(nullptr) + 400;
	std::map<std::string, std::string> bad_filter;
	bad_filter["bad key!"] = "value";  // space and ! are invalid

	SQLite3_result *result = stats->query_tsdb_metrics("some_metric", bad_filter, now - 10, now + 10);
	ok(result == nullptr, "query_tsdb_metrics: invalid label key returns null (valid_label_key rejects it)");
}

// ============================================================
// TSDB backend health
// ============================================================

static void test_insert_and_query_backend_health() {
	time_t now = time(nullptr) + 500;

	stats->insert_backend_health(1, "db1.example.com", 3306, true, 5, now);
	stats->insert_backend_health(1, "db2.example.com", 3306, false, -1, now);
	stats->insert_backend_health(2, "db3.example.com", 5432, true, 2, now);

	SQLite3_result *result = stats->get_backend_health_metrics(now - 10, now + 10);
	ok(result != nullptr, "get_backend_health_metrics: returns non-null for inserted data");
	if (result) {
		ok(result->rows_count == 3, "get_backend_health_metrics: returns 3 rows (got %lu)",
		   (unsigned long)result->rows_count);
		delete result;
	} else {
		ok(false, "get_backend_health_metrics: returns 3 rows (null result)");
	}

	// Filter by hostgroup
	result = stats->get_backend_health_metrics(now - 10, now + 10, 1);
	ok(result != nullptr, "get_backend_health_metrics: hostgroup filter returns non-null");
	if (result) {
		ok(result->rows_count == 2, "get_backend_health_metrics: hostgroup=1 returns 2 rows (got %lu)",
		   (unsigned long)result->rows_count);
		delete result;
	} else {
		ok(false, "get_backend_health_metrics: hostgroup=1 returns 2 rows (null result)");
	}
}

// ============================================================
// TSDB status
// ============================================================

static void test_get_tsdb_status() {
	// We have already inserted some metrics above
	ProxySQL_Statistics::tsdb_status_t st = stats->get_tsdb_status();
	ok(st.total_datapoints > 0, "get_tsdb_status: total_datapoints > 0 (got %zu)", st.total_datapoints);
	ok(st.total_series > 0, "get_tsdb_status: total_series > 0 (got %zu)", st.total_series);
	ok(st.newest_datapoint > 0, "get_tsdb_status: newest_datapoint is set");
}

// ============================================================
// TSDB downsampling
// ============================================================

static void test_tsdb_downsample_disabled() {
	stats->set_variable("enabled", "0");
	// Should just return without error
	stats->tsdb_downsample_metrics();
	ok(true, "tsdb_downsample_metrics: does not crash when TSDB disabled");
}

static void test_tsdb_retention_cleanup_disabled() {
	stats->set_variable("enabled", "0");
	stats->tsdb_retention_cleanup();
	ok(true, "tsdb_retention_cleanup: does not crash when TSDB disabled");
}

static void test_tsdb_downsample_runs() {
	stats->set_variable("enabled", "1");
	// Insert some metrics at specific bucket-aligned times
	time_t hour_start = (time(nullptr) / 3600) * 3600 - 7200;  // 2 hours ago
	std::map<std::string, std::string> labels;
	for (int i = 0; i < 10; i++) {
		stats->insert_tsdb_metric("downsample_test", labels, (double)i, hour_start + i * 60);
	}

	stats->tsdb_downsample_metrics();
	ok(true, "tsdb_downsample_metrics: runs without error on data");

	// Check that hourly data was created
	SQLite3_result *resultset = nullptr;
	char *error = nullptr;
	int cols = 0, affected_rows = 0;
	stats->statsdb_disk->execute_statement(
		"SELECT COUNT(*) FROM tsdb_metrics_hour WHERE metric_name='downsample_test'",
		&error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	bool has_hourly = false;
	if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) {
		has_hourly = atoi(resultset->rows[0]->fields[0]) > 0;
	}
	ok(has_hourly, "tsdb_downsample_metrics: creates hourly aggregated data");
	delete resultset;
}

static void test_tsdb_retention_cleanup_runs() {
	stats->set_variable("enabled", "1");
	stats->set_variable("retention_days", "1");

	// Insert old data
	time_t old_time = time(nullptr) - 86400 * 5;  // 5 days ago
	std::map<std::string, std::string> labels;
	stats->insert_tsdb_metric("old_metric", labels, 1.0, old_time);

	stats->tsdb_retention_cleanup();

	// Verify old data was removed
	SQLite3_result *resultset = nullptr;
	char *error = nullptr;
	int cols = 0, affected_rows = 0;
	char buf[256];
	snprintf(buf, sizeof(buf),
		"SELECT COUNT(*) FROM tsdb_metrics WHERE metric_name='old_metric' AND timestamp < %ld",
		(long)(time(nullptr) - 86400));
	stats->statsdb_disk->execute_statement(buf, &error, &cols, &affected_rows, &resultset);
	if (error) { free(error); error = nullptr; }
	bool old_removed = false;
	if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) {
		old_removed = atoi(resultset->rows[0]->fields[0]) == 0;
	}
	ok(old_removed, "tsdb_retention_cleanup: removes data older than retention_days");
	delete resultset;
}

#endif // PROXYSQLTSDB

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

int main() {
	// Count tests
	int num_tests = 0;

	// Constructor + init tests: 5
	num_tests += 5;
	// Timer tests: 15
	num_tests += 15;

#ifdef PROXYSQLTSDB
	// TSDB table init: 3
	num_tests += 3;
	// Variable management: 42
	num_tests += 42;
	// TSDB timers: 9
	num_tests += 9;
	// TSDB metric insert/query: 9
	num_tests += 9;
	// TSDB backend health: 4
	num_tests += 4;
	// TSDB status: 3
	num_tests += 3;
	// TSDB downsampling/retention: 5
	num_tests += 5;
#endif

	plan(num_tests);

	test_init_minimal();
	setup_stats();

	// Constructor + init
	test_constructor_creates_databases();
	test_init_creates_tables();

	// Timer tests
	test_timer_returns_false_when_disabled();
	test_timer_returns_true_on_first_call();
	test_timer_returns_false_before_interval();
	test_timer_returns_true_after_interval();
	test_query_cache_timer_disabled();
	test_query_cache_timer_triggers();
	test_system_cpu_timer_disabled();
	test_system_cpu_timer_triggers();
	test_mysql_eventslog_timer_disabled();
	test_mysql_eventslog_timer_triggers();
	test_mysql_eventslog_timer_no_retrigger();
	test_pgsql_eventslog_timer_disabled();
	test_pgsql_eventslog_timer_triggers();
	test_digest_to_disk_timer_disabled();
	test_digest_to_disk_timer_triggers();

#ifdef PROXYSQLTSDB
	// TSDB table init
	test_init_creates_tsdb_tables();

	// Variable management
	test_set_variable_enabled();
	test_set_variable_enabled_out_of_range();
	test_set_variable_sample_interval();
	test_set_variable_retention_days();
	test_set_variable_monitor_enabled();
	test_set_variable_monitor_interval();
	test_set_variable_invalid_name();
	test_set_variable_null_inputs();
	test_set_variable_non_integer_value();
	test_set_variable_case_insensitive();
	test_get_variable();
	test_get_variable_null_and_unknown();
	test_has_variable();
	test_get_variables_list();

	// TSDB timers
	test_tsdb_sampler_timer_disabled();
	test_tsdb_sampler_timer_triggers();
	test_tsdb_sampler_timer_no_retrigger();
	test_tsdb_downsample_timer();
	test_tsdb_monitor_timer();
	test_tsdb_retention_timer();

	// TSDB metric insert/query
	test_insert_and_query_tsdb_metric();
	test_query_tsdb_metric_with_label_filter();
	test_query_tsdb_metric_with_single_quote_in_name();
	test_query_tsdb_metric_swapped_time_range();
	test_query_tsdb_metric_invalid_label_key();

	// TSDB backend health
	test_insert_and_query_backend_health();

	// TSDB status
	test_get_tsdb_status();

	// TSDB downsampling/retention
	test_tsdb_downsample_disabled();
	test_tsdb_retention_cleanup_disabled();
	test_tsdb_downsample_runs();
	test_tsdb_retention_cleanup_runs();
#endif

	teardown_stats();
	test_cleanup_minimal();

	return exit_status();
}
