<?php
declare(strict_types=1);

if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

$root = dirname(__DIR__);
require_once $root . '/app/helpers.php';
require_once $root . '/app/Db.php';
require_once $root . '/app/ChildDatabaseManager.php';
require_once $root . '/templates/mirza_ehsannet/tenant_db.php';

$failures = [];
$assert = static function (bool $condition, string $message) use (&$failures): void {
    if (!$condition) $failures[] = $message;
};
$assertThrows = static function (callable $callback, string $message) use (&$failures): void {
    try {
        $callback();
        $failures[] = $message;
    } catch (Throwable) {
        // Expected: the scope firewall must fail closed.
    }
};

$testStorage = 'storage/.namespace_selftest';
$manager = new ChildDatabaseManager([
    'app_key' => bin2hex(random_bytes(32)),
    'db' => [
        'host' => 'localhost',
        'port' => 3306,
        'name' => 'mother_database',
        'user' => 'mother_user',
        'pass' => 'unused',
        'charset' => 'utf8mb4',
    ],
    // Deliberately use a broken legacy driver. It must be ignored.
    'child_db' => [
        'driver' => 'cpanel_cli',
        'uapi_binary' => '/definitely/missing/uapi',
        'storage_root' => $testStorage,
    ],
]);

$seenPrefixes = [];
for ($i = 0; $i < 1000; $i++) {
    $secret = bin2hex(random_bytes(24));
    $assert($manager->databaseNameForSecret($secret) === 'mother_database', 'Child must reuse only the configured mother database.');
    $assert($manager->databaseUserForSecret($secret) === 'mother_user', 'Child must reuse only the configured mother database account.');
    $prefix = $manager->tablePrefixForSecret($secret);
    $assert((bool)preg_match('/^[a-z][a-z0-9_]{8,30}_$/', $prefix), 'Invalid generated table namespace.');
    $assert(!isset($seenPrefixes[$prefix]), 'Table-namespace collision detected.');
    $seenPrefixes[$prefix] = true;
}

$secret = bin2hex(random_bytes(24));
$prefix = $manager->tablePrefixForSecret($secret);
$reflection = new ReflectionClass($manager);
$createRegistry = $reflection->getMethod('createStorageRegistry');
$createRegistry->setAccessible(true);
$removeRegistry = $reflection->getMethod('removeStorageRegistry');
$removeRegistry->setAccessible(true);
$relativePath = (string)$createRegistry->invoke($manager, $secret, [
    'name' => 'mother_database',
    'user' => 'mother_user',
    'provider' => 'table_namespace',
    'table_prefix' => $prefix,
]);
$manifestFile = $root . '/' . ltrim($relativePath, '/') . '/manifest.json';
$assert(is_file($manifestFile), 'Per-bot manifest was not created.');
$manifest = is_file($manifestFile) ? json_decode((string)file_get_contents($manifestFile), true) : null;
$assert(is_array($manifest), 'Per-bot manifest is not valid JSON.');
$assert(($manifest['provider'] ?? null) === 'table_namespace', 'Manifest provider is not table_namespace.');
$assert(($manifest['table_prefix'] ?? null) === $prefix, 'Manifest namespace does not match.');
$assert(!is_array($manifest) || !array_key_exists('password', $manifest), 'Manifest must never store a database password.');
$assert(is_dir(dirname($manifestFile) . '/backups'), 'Per-bot backups directory was not created.');
$removeRegistry->invoke($manager, $secret);

$GLOBALS['tenant_table_prefix'] = 'mb1234567890abcd_';
$rewriteCases = [
    'SELECT * FROM user' => 'SELECT * FROM mb1234567890abcd_user',
    'SELECT i.id FROM invoice i JOIN user u ON u.id=i.id_user' => 'SELECT i.id FROM mb1234567890abcd_invoice i JOIN mb1234567890abcd_user u ON u.id=i.id_user',
    'INSERT IGNORE INTO Payment_report(id_order) VALUES(1)' => 'INSERT IGNORE INTO mb1234567890abcd_Payment_report(id_order) VALUES(1)',
    'UPDATE setting SET Bot_Status=1' => 'UPDATE mb1234567890abcd_setting SET Bot_Status=1',
    'DELETE FROM product WHERE id=1' => 'DELETE FROM mb1234567890abcd_product WHERE id=1',
    'ALTER TABLE `invoice` ADD note TEXT' => 'ALTER TABLE `mb1234567890abcd_invoice` ADD note TEXT',
    'SHOW COLUMNS FROM invoice LIKE \'note\'' => 'SHOW COLUMNS FROM mb1234567890abcd_invoice LIKE \'note\'',
    'SHOW TABLES LIKE \'invoice\'' => "SHOW TABLES LIKE 'mb1234567890abcd\\_invoice'",
    'SELECT `setting` FROM x_ui' => 'SELECT `setting` FROM mb1234567890abcd_x_ui',
    'SELECT * FROM information_schema.tables' => 'SELECT * FROM information_schema.tables',
    'SELECT * FROM mb1234567890abcd_user' => 'SELECT * FROM mb1234567890abcd_user',
];
foreach ($rewriteCases as $input => $expected) {
    $actual = tenant_rewrite_sql($input);
    $assert($actual === $expected, "SQL rewrite mismatch: {$input} => {$actual}");
}
$assert(str_starts_with(tenant_rewrite_sql('SHOW TABLES'), "SHOW TABLES LIKE 'mb1234567890abcd"), 'SHOW TABLES is not restricted to the current namespace.');
$assert(tenant_rewrite_sql("INSERT INTO cron_runtime_state(job_key) VALUES('x') ON DUPLICATE KEY UPDATE unit=VALUES(unit)") === "INSERT INTO mb1234567890abcd_cron_runtime_state(job_key) VALUES('x') ON DUPLICATE KEY UPDATE unit=VALUES(unit)", 'ON DUPLICATE KEY UPDATE was misidentified as a table UPDATE.');
$assert(tenant_rewrite_sql('SELECT id, username, Balance FROM user ORDER BY register DESC LIMIT 10') === 'SELECT id, username, Balance FROM mb1234567890abcd_user ORDER BY register DESC LIMIT 10', 'Column commas or DESC/LIMIT confused the SQL firewall.');
$assertThrows(static fn() => tenant_rewrite_sql('SELECT * FROM users'), 'Mother table access was not blocked.');
$assertThrows(static fn() => tenant_rewrite_sql('SELECT * FROM mbffffffffffffff_user'), 'Another child namespace was not blocked.');
$assertThrows(static fn() => tenant_rewrite_sql('SELECT * FROM user u, bots b WHERE b.id=u.id'), 'Old-style comma access to a mother table was not blocked.');
$assertThrows(static fn() => tenant_rewrite_sql('SELECT * FROM other_database.user'), 'Cross-database access was not blocked.');

foreach (array_values(tenant_known_tables()) as $table) {
    $rewritten = tenant_rewrite_sql("SELECT * FROM `{$table}`");
    $assert(str_contains($rewritten, '`mb1234567890abcd_' . $table . '`'), "Known table was not namespaced: {$table}");
}

$installer = (string)file_get_contents($root . '/public/install.php');
$managerSource = (string)file_get_contents($root . '/app/ChildDatabaseManager.php');
$service = (string)file_get_contents($root . '/app/BotService.php');
$configSample = (string)file_get_contents($root . '/app/config.sample.php');
$backupSource = (string)file_get_contents($root . '/templates/mirza_ehsannet/cronbot/backupbot.php');
$assert(!str_contains($installer, 'name="cpanel_'), 'Installer still asks for cPanel settings.');
$assert(!str_contains($installer, 'name="child_db_'), 'Installer still asks for child database settings.');
$assert(str_contains($installer, "'driver' => 'table_namespace'"), 'Installer does not force zero-input isolation.');
$assert(!str_contains($managerSource, '/usr/local/cpanel'), 'Runtime still depends on a cPanel binary.');
$assert(!preg_match('/->exec\s*\(.*CREATE\s+USER/is', $managerSource), 'Runtime still tries to create a MySQL user.');
$assert(!preg_match('/->exec\s*\(.*CREATE\s+DATABASE/is', $managerSource), 'Runtime still tries to create a child database.');
$assert(str_contains($service, "foreach (['installer', 'install.sh'"), 'Generated child deployment does not remove unsafe legacy files defensively.');
$assert(!is_dir($root . '/templates/mirza_ehsannet/installer'), 'Legacy child installer is still packaged.');
$assert(!is_file($root . '/templates/mirza_ehsannet/install.sh'), 'Legacy child shell installer is still packaged.');
$assert(str_contains($configSample, "'driver' => 'table_namespace'"), 'Sample config is not zero-input.');
$assert(!preg_match('/\$command\s*=\s*[\"\'][^\n]*mysqldump/i', $backupSource), 'Child backup still invokes mysqldump.');

$templateRoot = $root . '/templates/mirza_ehsannet';
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($templateRoot, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $file) {
    if (!$file->isFile() || strtolower($file->getExtension()) !== 'php') continue;
    $path = str_replace('\\', '/', $file->getPathname());
    if (str_contains($path, '/vendor/') || str_contains($path, '/installer/')) continue;
    $source = (string)file_get_contents($file->getPathname());
    if (!str_ends_with($path, '/tenant_db.php')) {
        $assert(!preg_match('/\bmysqli_query\s*\(/', $source), 'Unwrapped mysqli_query found: ' . $path);
    }
    $assert(!preg_match('/new\s+PDO\s*\(/', $source), 'Raw PDO constructor found: ' . $path);
    $assert(!preg_match('/new\s+mysqli\s*\(/i', $source), 'Raw mysqli constructor found: ' . $path);
    $assert(!preg_match('/mysqli_connect\s*\(/i', $source), 'Raw mysqli_connect found: ' . $path);
}

$schema = (string)file_get_contents($root . '/sql/schema.sql');
$repository = (string)file_get_contents($root . '/app/Repository.php');
$assert(str_contains($schema, 'child_db_provider'), 'Schema is missing child_db_provider.');
$assert(str_contains($schema, 'child_storage_path'), 'Schema is missing child_storage_path.');
$assert(str_contains($repository, "'table_namespace'"), 'Repository does not persist table_namespace isolation.');
$assert(is_file($root . '/tools/upgrade_zero_input.php'), 'Zero-input upgrade helper is missing.');
$assert(is_file($root . '/tools/check_isolation.php'), 'Isolation check helper is missing.');

$testRoot = $root . '/storage/.namespace_selftest';
if (is_dir($testRoot)) {
    $removeTree = static function (string $dir) use (&$removeTree): void {
        foreach (scandir($dir) ?: [] as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . '/' . $item;
            if (is_dir($path) && !is_link($path)) $removeTree($path); else @unlink($path);
        }
        @rmdir($dir);
    };
    $removeTree($testRoot);
}

if ($failures) {
    foreach (array_values(array_unique($failures)) as $failure) fwrite(STDERR, "FAILED: {$failure}\n");
    exit(1);
}

fwrite(STDOUT, "OK: zero-input namespace isolation static checks passed.\n");
