/**
 * @file admin_disk_upgrade_unit-t.cpp
 * @brief Unit tests for ProxySQL_Admin disk upgrade functions.
 *
 * Tests the schema migration functions in ProxySQL_Admin_Disk_Upgrade.cpp:
 *   - disk_upgrade_scheduler()
 *   - disk_upgrade_mysql_users()
 *   - disk_upgrade_mysql_servers()
 *   - disk_upgrade_rest_api_routes()
 *   - disk_upgrade_mysql_query_rules()
 *   - disk_upgrade_pgsql_replication_hostgroups()
 *
 * Each function upgrades old table schemas to the current version by:
 *   1. Detecting the old schema via check_table_structure()
 *   2. Renaming the old table
 *   3. Creating a new table with the current schema
 *   4. Copying data from the old table to the new one
 *
 * Strategy: We cannot call the ProxySQL_Admin constructor (it requires
 * many initialized globals). Instead we allocate raw memory, zero it,
 * set configdb to an in-memory SQLite3DB, and call the upgrade methods
 * via a friend class wrapper. The upgrade methods only access this->configdb.
 */

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

#include "cpp.h"
#include "ProxySQL_Admin_Tables_Definitions.h"

#include <cstring>
#include <string>
#include <cstdlib>

// ---------------------------------------------------------------------------
// TestDiskUpgrade: friend class that accesses private disk_upgrade methods
// ---------------------------------------------------------------------------

/**
 * @brief Friend class wrapper for ProxySQL_Admin private disk_upgrade methods.
 *
 * The ProxySQL_Admin constructor requires many initialized globals
 * (GloProxyStats, scheduler, etc.) that are unavailable in the unit test
 * harness. Since the disk_upgrade_* methods only use this->configdb, we
 * allocate zeroed memory (skipping the constructor) and set configdb to
 * an in-memory SQLite3DB.
 */
class TestDiskUpgrade {
public:
	ProxySQL_Admin *admin;

	TestDiskUpgrade() {
		// NOTE: We intentionally skip the ProxySQL_Admin constructor because it
		// requires GloProxyStats, GloAdmin, and other daemon globals that are
		// unavailable in unit tests. The disk_upgrade_*() methods only access
		// this->configdb (a SQLite3DB pointer), so zeroed memory + setting
		// configdb is sufficient for these tests. This is technically UB in
		// C++ but is a pragmatic choice for test-only code.
		void *mem = calloc(1, sizeof(ProxySQL_Admin));
		admin = reinterpret_cast<ProxySQL_Admin*>(mem);
		SQLite3DB *db_ = new SQLite3DB();
		db_->open((char *)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX);
		admin->configdb = db_;
	}

	~TestDiskUpgrade() {
		if (admin->configdb) {
			delete admin->configdb;
			admin->configdb = nullptr;
		}
		free(admin);
	}

	SQLite3DB* db() { return admin->configdb; }

	void upgrade_scheduler() { admin->disk_upgrade_scheduler(); }
	void upgrade_mysql_users() { admin->disk_upgrade_mysql_users(); }
	void upgrade_mysql_servers() { admin->disk_upgrade_mysql_servers(); }
	void upgrade_rest_api_routes() { admin->disk_upgrade_rest_api_routes(); }
	void upgrade_mysql_query_rules() { admin->disk_upgrade_mysql_query_rules(); }
	void upgrade_pgsql_replication_hostgroups() { admin->disk_upgrade_pgsql_replication_hostgroups(); }
};

// ---------------------------------------------------------------------------
// Helper: query a single integer from the database
// ---------------------------------------------------------------------------
static int query_int(SQLite3DB *db, const char *sql) {
	return db->return_one_int(sql);
}

// ---------------------------------------------------------------------------
// Helper: query a single string value from the database
// ---------------------------------------------------------------------------
static std::string query_string(SQLite3DB *db, const char *sql) {
	char *error = nullptr;
	SQLite3_result *result = db->execute_statement(sql, &error);
	std::string val;
	if (result && result->rows_count > 0 && result->rows[0]->fields[0]) {
		val = result->rows[0]->fields[0];
	}
	if (error) {
		free(error);
	}
	delete result;
	return val;
}

// ---------------------------------------------------------------------------
// Helper: check that a table matches the current (latest) schema
// ---------------------------------------------------------------------------
static bool table_matches_current(SQLite3DB *db, const char *tbl, const char *def) {
	return db->check_table_structure(tbl, def) == 1;
}

// ============================================================================
// disk_upgrade_scheduler() tests
// ============================================================================

static void test_scheduler_upgrade_from_v1_2_0() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_SCHEDULER_V1_2_0);
	db->execute("INSERT INTO scheduler (id, interval_ms, filename) VALUES (1, 1000, '/usr/bin/test.sh')");
	db->execute("INSERT INTO scheduler (id, interval_ms, filename, arg1) VALUES (2, 5000, '/usr/bin/check.sh', '--verbose')");

	t.upgrade_scheduler();

	ok(table_matches_current(db, "scheduler", ADMIN_SQLITE_TABLE_SCHEDULER),
		"scheduler: upgrade from v1.2.0 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM scheduler");
	ok(count == 2, "scheduler: v1.2.0 upgrade preserves 2 rows (got %d)", count);

	std::string filename = query_string(db, "SELECT filename FROM scheduler WHERE id=1");
	ok(filename == "/usr/bin/test.sh",
		"scheduler: v1.2.0 upgrade preserves filename field");

	std::string arg1 = query_string(db, "SELECT arg1 FROM scheduler WHERE id=2");
	ok(arg1 == "--verbose",
		"scheduler: v1.2.0 upgrade preserves arg1 field");

	int old_exists = query_int(db, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='scheduler_v120'");
	ok(old_exists == 1, "scheduler: v1.2.0 old table renamed to scheduler_v120");
}

static void test_scheduler_upgrade_from_v1_2_2a() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_SCHEDULER_V1_2_2a);
	db->execute("INSERT INTO scheduler (id, interval_ms, filename, comment) VALUES (1, 2000, '/usr/bin/job.sh', 'nightly job')");

	t.upgrade_scheduler();

	ok(table_matches_current(db, "scheduler", ADMIN_SQLITE_TABLE_SCHEDULER),
		"scheduler: upgrade from v1.2.2a produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM scheduler");
	ok(count == 1, "scheduler: v1.2.2a upgrade preserves 1 row (got %d)", count);

	std::string comment = query_string(db, "SELECT comment FROM scheduler WHERE id=1");
	ok(comment == "nightly job",
		"scheduler: v1.2.2a upgrade preserves comment field");
}

static void test_scheduler_upgrade_from_v1_2_2b() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_SCHEDULER_V1_2_2b);
	db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, comment) VALUES (1, 1, 3000, '/usr/bin/task.sh', 'test task')");

	t.upgrade_scheduler();

	ok(table_matches_current(db, "scheduler", ADMIN_SQLITE_TABLE_SCHEDULER),
		"scheduler: upgrade from v1.2.2b produces current schema");

	int active = query_int(db, "SELECT active FROM scheduler WHERE id=1");
	ok(active == 1, "scheduler: v1.2.2b upgrade preserves active field (got %d)", active);
}

static void test_scheduler_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("scheduler", ADMIN_SQLITE_TABLE_SCHEDULER, false);
	db->execute("INSERT INTO scheduler (id, active, interval_ms, filename, comment) VALUES (1, 1, 1000, '/usr/bin/ok.sh', 'current')");

	t.upgrade_scheduler();

	ok(table_matches_current(db, "scheduler", ADMIN_SQLITE_TABLE_SCHEDULER),
		"scheduler: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM scheduler");
	ok(count == 1, "scheduler: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// disk_upgrade_mysql_users() tests
// ============================================================================

static void test_mysql_users_upgrade_from_v1_3_0() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS_V1_3_0);
	db->execute("INSERT INTO mysql_users (username, password, active, use_ssl, default_hostgroup, default_schema, "
		"schema_locked, transaction_persistent, fast_forward, backend, frontend, max_connections) "
		"VALUES ('testuser', 'secret123', 1, 0, 0, 'test_db', 0, 0, 0, 1, 1, 5000)");

	t.upgrade_mysql_users();

	ok(table_matches_current(db, "mysql_users", ADMIN_SQLITE_TABLE_MYSQL_USERS),
		"mysql_users: upgrade from v1.3.0 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_users");
	ok(count == 1, "mysql_users: v1.3.0 upgrade preserves 1 row (got %d)", count);

	std::string username = query_string(db, "SELECT username FROM mysql_users");
	ok(username == "testuser", "mysql_users: v1.3.0 upgrade preserves username");

	std::string password = query_string(db, "SELECT password FROM mysql_users");
	ok(password == "secret123", "mysql_users: v1.3.0 upgrade preserves password");

	int max_conn = query_int(db, "SELECT max_connections FROM mysql_users");
	ok(max_conn == 5000, "mysql_users: v1.3.0 upgrade preserves max_connections (got %d)", max_conn);

	int old_exists = query_int(db, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='mysql_users_v130'");
	ok(old_exists == 1, "mysql_users: v1.3.0 old table renamed to mysql_users_v130");
}

static void test_mysql_users_upgrade_from_v1_4_0() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS_V1_4_0);
	db->execute("INSERT INTO mysql_users (username, password, active, use_ssl, default_hostgroup, default_schema, "
		"schema_locked, transaction_persistent, fast_forward, backend, frontend, max_connections) "
		"VALUES ('user2', 'pass2', 1, 0, 1, NULL, 0, 1, 0, 1, 1, 10000)");

	t.upgrade_mysql_users();

	ok(table_matches_current(db, "mysql_users", ADMIN_SQLITE_TABLE_MYSQL_USERS),
		"mysql_users: upgrade from v1.4.0 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_users");
	ok(count == 1, "mysql_users: v1.4.0 upgrade preserves 1 row (got %d)", count);
}

static void test_mysql_users_upgrade_from_v2_0_0() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_USERS_V2_0_0);
	db->execute("INSERT INTO mysql_users (username, password, active, use_ssl, default_hostgroup, default_schema, "
		"schema_locked, transaction_persistent, fast_forward, backend, frontend, max_connections, comment) "
		"VALUES ('user3', 'pass3', 1, 0, 2, 'mydb', 0, 1, 0, 1, 1, 8000, 'test comment')");

	t.upgrade_mysql_users();

	ok(table_matches_current(db, "mysql_users", ADMIN_SQLITE_TABLE_MYSQL_USERS),
		"mysql_users: upgrade from v2.0.0 produces current schema");

	std::string comment = query_string(db, "SELECT comment FROM mysql_users WHERE username='user3'");
	ok(comment == "test comment", "mysql_users: v2.0.0 upgrade preserves comment field");
}

static void test_mysql_users_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("mysql_users", ADMIN_SQLITE_TABLE_MYSQL_USERS, false);
	db->execute("INSERT INTO mysql_users (username, password, backend, frontend) VALUES ('current', 'pw', 1, 1)");

	t.upgrade_mysql_users();

	ok(table_matches_current(db, "mysql_users", ADMIN_SQLITE_TABLE_MYSQL_USERS),
		"mysql_users: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_users");
	ok(count == 1, "mysql_users: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// disk_upgrade_rest_api_routes() tests
// ============================================================================

static void test_restapi_routes_upgrade_from_v2_0_15() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_RESTAPI_ROUTES_V2_0_15);
	db->execute("INSERT INTO restapi_routes (id, active, interval_ms, method, uri, script, comment) "
		"VALUES (1, 1, 5000, 'GET', '/v1/health', '/usr/bin/health.sh', 'health check')");

	t.upgrade_rest_api_routes();

	ok(table_matches_current(db, "restapi_routes", ADMIN_SQLITE_TABLE_RESTAPI_ROUTES),
		"restapi_routes: upgrade from v2.0.15 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM restapi_routes");
	ok(count == 1, "restapi_routes: v2.0.15 upgrade preserves 1 row (got %d)", count);

	int timeout = query_int(db, "SELECT timeout_ms FROM restapi_routes WHERE id=1");
	ok(timeout == 5000, "restapi_routes: interval_ms mapped to timeout_ms (got %d)", timeout);

	std::string uri = query_string(db, "SELECT uri FROM restapi_routes WHERE id=1");
	ok(uri == "/v1/health", "restapi_routes: v2.0.15 upgrade preserves uri field");

	std::string method = query_string(db, "SELECT method FROM restapi_routes WHERE id=1");
	ok(method == "GET", "restapi_routes: v2.0.15 upgrade preserves method field");

	int old_exists = query_int(db, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='restapi_routes_v2015'");
	ok(old_exists == 1, "restapi_routes: v2.0.15 old table renamed to restapi_routes_v2015");
}

static void test_restapi_routes_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("restapi_routes", ADMIN_SQLITE_TABLE_RESTAPI_ROUTES, false);
	db->execute("INSERT INTO restapi_routes (id, active, timeout_ms, method, uri, script) "
		"VALUES (1, 1, 3000, 'POST', '/v1/action', '/usr/bin/action.sh')");

	t.upgrade_rest_api_routes();

	ok(table_matches_current(db, "restapi_routes", ADMIN_SQLITE_TABLE_RESTAPI_ROUTES),
		"restapi_routes: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM restapi_routes");
	ok(count == 1, "restapi_routes: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// disk_upgrade_mysql_servers() tests
// ============================================================================

static void test_mysql_servers_upgrade_from_v1_1_0() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_SERVERS_V1_1_0);
	// Insert data with weight > 10000000 (test bug #1224 fix) and compression > 1 (test bug #962 fix)
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, compression, max_connections, max_replication_lag) "
		"VALUES (0, '127.0.0.1', 3306, 'ONLINE', 99999999, 2, 100, 0)");
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, compression, max_connections, max_replication_lag) "
		"VALUES (1, '127.0.0.2', 3306, 'ONLINE', 1000, 0, 200, 5)");

	t.upgrade_mysql_servers();

	ok(table_matches_current(db, "mysql_servers", ADMIN_SQLITE_TABLE_MYSQL_SERVERS),
		"mysql_servers: upgrade from v1.1.0 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_servers");
	ok(count == 2, "mysql_servers: v1.1.0 upgrade preserves 2 rows (got %d)", count);

	int weight = query_int(db, "SELECT weight FROM mysql_servers WHERE hostname='127.0.0.1'");
	ok(weight == 10000000, "mysql_servers: v1.1.0 upgrade caps weight at 10000000 (got %d)", weight);

	int compression = query_int(db, "SELECT compression FROM mysql_servers WHERE hostname='127.0.0.1'");
	ok(compression == 1, "mysql_servers: v1.1.0 upgrade caps compression at 1 (got %d)", compression);

	std::string hostname = query_string(db, "SELECT hostname FROM mysql_servers WHERE hostgroup_id=1");
	ok(hostname == "127.0.0.2", "mysql_servers: v1.1.0 upgrade preserves hostname");
}

static void test_mysql_servers_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("mysql_servers", ADMIN_SQLITE_TABLE_MYSQL_SERVERS, false);
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (0, '10.0.0.1', 3306)");

	t.upgrade_mysql_servers();

	ok(table_matches_current(db, "mysql_servers", ADMIN_SQLITE_TABLE_MYSQL_SERVERS),
		"mysql_servers: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_servers");
	ok(count == 1, "mysql_servers: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// disk_upgrade_pgsql_replication_hostgroups() tests
// ============================================================================

static void test_pgsql_repl_hg_upgrade_from_v3_0_1() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_PGSQL_REPLICATION_HOSTGROUPS_V3_0_1);
	db->execute("INSERT INTO pgsql_replication_hostgroups (writer_hostgroup, reader_hostgroup, comment) "
		"VALUES (10, 20, 'primary-replica pair')");

	t.upgrade_pgsql_replication_hostgroups();

	ok(table_matches_current(db, "pgsql_replication_hostgroups", ADMIN_SQLITE_TABLE_PGSQL_REPLICATION_HOSTGROUPS),
		"pgsql_replication_hostgroups: upgrade from v3.0.1 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM pgsql_replication_hostgroups");
	ok(count == 1, "pgsql_replication_hostgroups: v3.0.1 upgrade preserves 1 row (got %d)", count);

	std::string check_type = query_string(db, "SELECT check_type FROM pgsql_replication_hostgroups WHERE writer_hostgroup=10");
	ok(check_type == "read_only",
		"pgsql_replication_hostgroups: v3.0.1 upgrade sets check_type to 'read_only'");

	std::string comment = query_string(db, "SELECT comment FROM pgsql_replication_hostgroups WHERE writer_hostgroup=10");
	ok(comment == "primary-replica pair",
		"pgsql_replication_hostgroups: v3.0.1 upgrade preserves comment field");

	int old_exists = query_int(db, "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='pgsql_replication_hostgroups_v301'");
	ok(old_exists == 1,
		"pgsql_replication_hostgroups: v3.0.1 old table renamed to pgsql_replication_hostgroups_v301");
}

static void test_pgsql_repl_hg_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("pgsql_replication_hostgroups", ADMIN_SQLITE_TABLE_PGSQL_REPLICATION_HOSTGROUPS, false);
	db->execute("INSERT INTO pgsql_replication_hostgroups (writer_hostgroup, reader_hostgroup, check_type, comment) "
		"VALUES (30, 40, 'read_only', 'already current')");

	t.upgrade_pgsql_replication_hostgroups();

	ok(table_matches_current(db, "pgsql_replication_hostgroups", ADMIN_SQLITE_TABLE_PGSQL_REPLICATION_HOSTGROUPS),
		"pgsql_replication_hostgroups: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM pgsql_replication_hostgroups");
	ok(count == 1, "pgsql_replication_hostgroups: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// disk_upgrade_mysql_query_rules() tests
// ============================================================================

static void test_mysql_query_rules_upgrade_from_v1_2_2() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES_V1_2_2);
	db->execute("INSERT INTO mysql_query_rules (rule_id, active, username, schemaname, flagIN, "
		"client_addr, proxy_addr, proxy_port, digest, match_digest, match_pattern, "
		"negate_match_pattern, flagOUT, replace_pattern, destination_hostgroup, cache_ttl, "
		"reconnect, timeout, retries, delay, mirror_flagOUT, mirror_hostgroup, error_msg, "
		"log, apply, comment) "
		"VALUES (1, 1, 'testuser', 'testdb', 0, '', '', NULL, '', '^SELECT', '', "
		"0, NULL, '', 1, 5000, "
		"0, 3000, 3, 0, NULL, NULL, '', "
		"0, 1, 'route reads')");

	t.upgrade_mysql_query_rules();

	ok(table_matches_current(db, "mysql_query_rules", ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES),
		"mysql_query_rules: upgrade from v1.2.2 produces current schema");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_query_rules");
	ok(count == 1, "mysql_query_rules: v1.2.2 upgrade preserves 1 row (got %d)", count);

	std::string username = query_string(db, "SELECT username FROM mysql_query_rules WHERE rule_id=1");
	ok(username == "testuser", "mysql_query_rules: v1.2.2 upgrade preserves username");

	int dest_hg = query_int(db, "SELECT destination_hostgroup FROM mysql_query_rules WHERE rule_id=1");
	ok(dest_hg == 1, "mysql_query_rules: v1.2.2 upgrade preserves destination_hostgroup (got %d)", dest_hg);

	std::string comment = query_string(db, "SELECT comment FROM mysql_query_rules WHERE rule_id=1");
	ok(comment == "route reads", "mysql_query_rules: v1.2.2 upgrade preserves comment");
}

static void test_mysql_query_rules_no_upgrade_needed() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->build_table("mysql_query_rules", ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES, false);
	db->execute("INSERT INTO mysql_query_rules (rule_id, active, apply) VALUES (1, 1, 1)");

	t.upgrade_mysql_query_rules();

	ok(table_matches_current(db, "mysql_query_rules", ADMIN_SQLITE_TABLE_MYSQL_QUERY_RULES),
		"mysql_query_rules: no upgrade needed when schema is current");

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_query_rules");
	ok(count == 1, "mysql_query_rules: data unchanged when no upgrade needed (got %d)", count);
}

// ============================================================================
// Multi-row data preservation tests
// ============================================================================

static void test_scheduler_upgrade_preserves_multiple_rows() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_SCHEDULER_V1_2_0);
	for (int i = 1; i <= 10; i++) {
		char sql[256];
		snprintf(sql, sizeof(sql),
			"INSERT INTO scheduler (id, interval_ms, filename) VALUES (%d, %d, '/usr/bin/job%d.sh')",
			i, 1000 * i, i);
		db->execute(sql);
	}

	t.upgrade_scheduler();

	int count = query_int(db, "SELECT COUNT(*) FROM scheduler");
	ok(count == 10, "scheduler: v1.2.0 upgrade preserves all 10 rows (got %d)", count);

	int interval = query_int(db, "SELECT interval_ms FROM scheduler WHERE id=5");
	ok(interval == 5000, "scheduler: v1.2.0 upgrade preserves interval_ms for row 5 (got %d)", interval);
}

static void test_mysql_servers_upgrade_multiple_rows_with_fixes() {
	TestDiskUpgrade t;
	SQLite3DB *db = t.db();

	db->execute(ADMIN_SQLITE_TABLE_MYSQL_SERVERS_V1_1_0);
	// Row with weight > 10000000 and compression > 1
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, compression, max_connections, max_replication_lag) "
		"VALUES (0, 'host1', 3306, 'ONLINE', 20000000, 5, 100, 0)");
	// Row with normal values
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, compression, max_connections, max_replication_lag) "
		"VALUES (0, 'host2', 3306, 'ONLINE', 500, 0, 50, 10)");
	// Row with compression=0 (should remain 0)
	db->execute("INSERT INTO mysql_servers (hostgroup_id, hostname, port, status, weight, compression, max_connections, max_replication_lag) "
		"VALUES (1, 'host3', 3307, 'SHUNNED', 1000, 0, 200, 0)");

	t.upgrade_mysql_servers();

	int count = query_int(db, "SELECT COUNT(*) FROM mysql_servers");
	ok(count == 3, "mysql_servers: v1.1.0 upgrade preserves all 3 rows (got %d)", count);

	int w1 = query_int(db, "SELECT weight FROM mysql_servers WHERE hostname='host1'");
	ok(w1 == 10000000, "mysql_servers: weight capped at 10000000 for over-weight row (got %d)", w1);

	int c1 = query_int(db, "SELECT compression FROM mysql_servers WHERE hostname='host1'");
	ok(c1 == 1, "mysql_servers: compression capped at 1 for over-compression row (got %d)", c1);

	int c3 = query_int(db, "SELECT compression FROM mysql_servers WHERE hostname='host3'");
	ok(c3 == 0, "mysql_servers: compression=0 not changed (got %d)", c3);

	int w2 = query_int(db, "SELECT weight FROM mysql_servers WHERE hostname='host2'");
	ok(w2 == 500, "mysql_servers: normal weight preserved (got %d)", w2);
}

// ============================================================================
// main
// ============================================================================

int main() {
	plan(60);
	test_init_minimal();

	// scheduler tests
	test_scheduler_upgrade_from_v1_2_0();
	test_scheduler_upgrade_from_v1_2_2a();
	test_scheduler_upgrade_from_v1_2_2b();
	test_scheduler_no_upgrade_needed();

	// mysql_users tests
	test_mysql_users_upgrade_from_v1_3_0();
	test_mysql_users_upgrade_from_v1_4_0();
	test_mysql_users_upgrade_from_v2_0_0();
	test_mysql_users_no_upgrade_needed();

	// restapi_routes tests
	test_restapi_routes_upgrade_from_v2_0_15();
	test_restapi_routes_no_upgrade_needed();

	// mysql_servers tests
	test_mysql_servers_upgrade_from_v1_1_0();
	test_mysql_servers_no_upgrade_needed();

	// pgsql_replication_hostgroups tests
	test_pgsql_repl_hg_upgrade_from_v3_0_1();
	test_pgsql_repl_hg_no_upgrade_needed();

	// mysql_query_rules tests
	test_mysql_query_rules_upgrade_from_v1_2_2();
	test_mysql_query_rules_no_upgrade_needed();

	// Multi-row tests
	test_scheduler_upgrade_preserves_multiple_rows();
	test_mysql_servers_upgrade_multiple_rows_with_fixes();

	test_cleanup_minimal();
	return exit_status();
}
