#!/usr/bin/env php
<?php
/**
 * Cloaka 完整回归 + PHP 8 兼容性检测
 *
 * 用法：php tools/regression_test.php [--verbose]
 */
if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "请在 CLI 下运行：php tools/regression_test.php\n");
    exit(1);
}

$root    = dirname(__DIR__);
$verbose = in_array('--verbose', $argv, true) || in_array('-v', $argv, true);

require_once $root . '/lib/CloakPhpCompat.php';

$passed = 0;
$failed = 0;
$skipped = 0;

function reg_assert($name, $condition, $detail = '')
{
    global $passed, $failed, $verbose;
    if ($condition) {
        $passed++;
        if ($verbose) {
            echo "  ✓ {$name}\n";
        }
        return true;
    }
    $failed++;
    echo "  ✗ FAIL: {$name}\n";
    if ($detail !== '') {
        echo "    {$detail}\n";
    }
    return false;
}

function reg_skip($name, $reason)
{
    global $skipped, $verbose;
    $skipped++;
    if ($verbose) {
        echo "  ○ SKIP: {$name} — {$reason}\n";
    }
}

echo "=== Cloaka 回归测试 ===\n";
echo 'PHP ' . PHP_VERSION . ' (' . PHP_SAPI . ")\n\n";

// ── 1. PHP 8 环境 ─────────────────────────────────────────────
echo "[1] PHP 8 兼容性\n";
$compat = CloakPhpCompat::buildReport();
reg_assert('PHP 版本 >= ' . CloakPhpCompat::MIN_PHP_VERSION, $compat['ok'], implode('; ', $compat['errors']));
foreach ($compat['warnings'] as $w) {
    if ($verbose) {
        echo "  ! WARN: {$w}\n";
    }
}

// ── 2. 语法检查 ───────────────────────────────────────────────
echo "[2] 全项目 php -l\n";
$lint = CloakPhpCompat::lintProject($root);
reg_assert('所有 PHP 文件语法正确', $lint['ok'], implode("\n    ", array_slice($lint['failures'], 0, 5)));

// ── 3. Bootstrap 链 ───────────────────────────────────────────
echo "[3] 引导与配置\n";
require_once $root . '/cong.php';
require_once $root . '/config/ConfigLoader.php';
require_once $root . '/lib/bootstrap.php';

ConfigLoader::loadByName('index', $root);
reg_assert('ConfigLoader 可加载 index 配置', defined('SHOW_SITE_MODE_SWITCH'));

reg_assert('bootstrap 已加载 CheckPipeline', class_exists('CheckPipeline', false));

// ── 4. ConfigWriter 输出 ──────────────────────────────────────
echo "[4] 后台 ConfigWriter\n";
require_once $root . '/admin/ConfigWriter.php';
$sample = ConfigWriter::buildDefineSource('index.php', [
    'methods' => 'ip_check', 'blacklist_group_id' => 0, 'whitelist_group_id' => 0,
    'country' => 'US', 'show_content' => '302', 'DB_fp' => ['https://x.com/'],
    'costm_ip_score' => 'test', 'auto_black' => 'OFF', 'is_virtual' => '0',
    'auto_black_times' => '5', 'white_params' => '', 'iphone_model' => 0,
    'write_log' => 0, 'zh_on' => 'OFF', 'os_on' => 'OFF', 'keep_params' => 'OFF',
    'redirect_method' => '302', 'DB_zp' => 'https://z.com', 'mobile_on' => 'OFF',
    'v_referer' => 'OFF', 'risk_number' => '0', 'risk_enhanced' => 'OFF', 'url_args_timeout' => '0',
    'debug_mode' => 'OFF',
    'ad_fb' => 'OFF', 'ad_google' => 'OFF', 'ad_tiktok' => 'OFF', 'ad_strict' => 'OFF', 'ad_spm_id' => '',
]);
$tmpCfg = sys_get_temp_dir() . '/cloak_reg_cfg_' . getmypid() . '.php';
file_put_contents($tmpCfg, $sample);
exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($tmpCfg) . ' 2>&1', $lintOut, $lintCode);
@unlink($tmpCfg);
reg_assert('ConfigWriter 生成合法 PHP', $lintCode === 0, implode(' ', $lintOut));
reg_assert('ConfigWriter 含 deprecated SHOW_SITE_MOBILE', strpos($sample, 'SHOW_SITE_MOBILE') !== false);
reg_assert('ConfigWriter 含 CLOAK_RISK_ENHANCED', strpos($sample, 'CLOAK_RISK_ENHANCED') !== false);
reg_assert('ConfigWriter 含 CLOAK_AD_FB', strpos($sample, 'CLOAK_AD_FB') !== false);
reg_assert('ConfigWriter 含 CLOAK_AD_SPM_ID', strpos($sample, 'CLOAK_AD_SPM_ID') !== false);

$adGuardOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_ad_source_guard.php') . ' 2>/dev/null');
$adGuardJson = json_decode((string) $adGuardOut, true);
reg_assert('广告来源 Guard 验证', ($adGuardJson['ok'] ?? false) === true, (string) $adGuardOut);

$timerOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_pipeline_timer.php') . ' 2>/dev/null');
$timerJson = json_decode((string) $timerOut, true);
reg_assert('PipelineTimer 验证', ($timerJson['ok'] ?? false) === true, (string) $timerOut);

$realVisitorOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_session_real_visitor.php') . ' 2>/dev/null');
$realVisitorJson = json_decode((string) $realVisitorOut, true);
reg_assert('真实访客 Session 缓存', ($realVisitorJson['ok'] ?? false) === true, (string) $realVisitorOut);

$reqUrlOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_request_url.php') . ' 2>/dev/null');
$reqUrlJson = json_decode((string) $reqUrlOut, true);
reg_assert('请求 URL 辅助函数', ($reqUrlJson['ok'] ?? false) === true, (string) $reqUrlOut);

// ── 5. CheckPipeline 三种模式 ─────────────────────────────────
echo "[5] CheckPipeline 模式\n";

function run_pipeline_subprocess($root, $fixtureName)
{
    $fixture = $root . '/tools/fixtures/' . $fixtureName;
    $cmd     = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_pipeline_fixture.php')
        . ' ' . escapeshellarg($fixture) . ' 2>&1';
    exec($cmd, $out, $code);
    $json = json_decode(implode("\n", $out), true);
    return is_array($json) ? $json : ['error' => implode("\n", $out), 'code' => $code];
}

$ipResult = run_pipeline_subprocess($root, 'regression_ip_check.php');
reg_assert('ip_check + fbclid 产生 check_result', ($ipResult['result'] ?? null) !== null, json_encode($ipResult, JSON_UNESCAPED_UNICODE));
reg_assert('ip_check 无意外 HTML 输出', (int) ($ipResult['output_len'] ?? -1) === 0);

$fpResult = run_pipeline_subprocess($root, 'regression_fp.php');
reg_assert('fp 模式配置生效', ($fpResult['mode'] ?? '') === 'fp', json_encode($fpResult, JSON_UNESCAPED_UNICODE));

$zpResult = run_pipeline_subprocess($root, 'regression_zp.php');
reg_assert('zp 模式配置生效', ($zpResult['mode'] ?? '') === 'zp', json_encode($zpResult, JSON_UNESCAPED_UNICODE));
reg_assert('zp 模式判定 false + 正品模式', ($zpResult['result'] ?? null) === 'false' && ($zpResult['reason'] ?? '') === '正品模式', json_encode($zpResult, JSON_UNESCAPED_UNICODE));

$earlyRouteOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_early_route_gate.php') . ' 2>/dev/null');
$earlyRouteJson = json_decode((string) $earlyRouteOut, true);
reg_assert('白名单/正品早期快速路径', ($earlyRouteJson['ok'] ?? false) === true, (string) $earlyRouteOut);

// ── 7. 指纹跳转模板 ───────────────────────────────────────────
echo "[7] 指纹跳转模板\n";
require_once $root . '/lib/Cloak/Page/FingerprintRedirectRenderer.php';
$tpl = $root . '/resources/fingerprint_redirect.html.php';
reg_assert('指纹模板文件存在', is_readable($tpl));
$tplBody = file_get_contents($tpl);
reg_assert('模板含 CONFIG_NAME 占位符', strpos($tplBody, '{{CONFIG_NAME}}') !== false);
reg_assert('模板 time 参数存在时停止重定向', strpos($tplBody, "urlParams.has('time')") !== false && strpos($tplBody, 'return;') !== false);
reg_assert('模板在加载 dist 前注册回调', strpos($tplBody, '__cloakFingerprintDone') !== false && strpos($tplBody, '<script src="/dist/?c={{CONFIG_NAME}}"></script>') !== false);

require_once $root . '/lib/CloakHttpHelpers.php';
$_GET = ['time' => '1234567890'];
reg_assert('cloak_fingerprint_time_param_present', cloak_fingerprint_time_param_present());
$_GET = [];
$_COOKIE = ['ctime' => '1', 'cyyek' => 'x', 'cl' => 'oklot'];
reg_assert('cloak_fingerprint_cookies_ready', cloak_fingerprint_cookies_ready());

$_SERVER['HTTP_HOST'] = 'shop.example.com';
reg_assert('cloak_fingerprint_cookie_domain', cloak_fingerprint_cookie_domain() === '.example.com');

$cookieDomainOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cookie_domain.php') . ' 2>/dev/null');
$cookieDomainJson = json_decode((string) $cookieDomainOut, true);
reg_assert('Cookie 域推断与 JS 选项', ($cookieDomainJson['ok'] ?? false) === true, (string) $cookieDomainOut);

$_GET = ['c' => 'index'];
ob_start();
include $root . '/dist/index.php';
$distJs = ob_get_clean();
reg_assert('dist/index.php 输出 Cookies.set', strpos($distJs, "fpCookies.set('ctime'") !== false);
reg_assert('dist/index.php 输出指纹完成回调', strpos($distJs, '__cloakFingerprintDone') !== false);
reg_assert('dist/index.php 输出 sameSite Lax', strpos($distJs, '"sameSite":"Lax"') !== false || strpos($distJs, '"sameSite": "Lax"') !== false);
reg_assert('dist/index.php 无 PHP Fatal', stripos($distJs, 'Fatal error') === false);

$virtualDetectOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_virtual_detect.php') . ' 2>/dev/null');
$virtualDetectJson = json_decode((string) $virtualDetectOut, true);
reg_assert('虚拟设备检测规则引擎', ($virtualDetectJson['ok'] ?? false) === true, (string) $virtualDetectOut);

$_COOKIE['mmr'] = '18';
reg_assert('mmr 18 映射', cloak_fingerprint_mmr_reason() === '移动端无有效加速度计/陀螺仪');
unset($_COOKIE['mmr']);

// ── 8. f_check JSON 契约（405） ──────────────────────────────
echo "[8] f_check API 边界\n";
$fcheckCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg(
    '$_SERVER["REQUEST_METHOD"]="GET"; include ' . var_export($root . '/f_check.php', true) . ';'
) . ' 2>/dev/null';
$fcheckOut = shell_exec($fcheckCmd);
$fcJson    = json_decode((string) $fcheckOut, true);
reg_assert('f_check 非 POST 返回 JSON status=error', is_array($fcJson) && ($fcJson['status'] ?? '') === 'error', (string) $fcheckOut);

$fcheckBadCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg(
    '$_SERVER["REQUEST_METHOD"]="POST"; $_POST=[]; include ' . var_export($root . '/f_check.php', true) . ';'
) . ' 2>/dev/null';
$fcheckBadOut = shell_exec($fcheckBadCmd);
$fcBadJson    = json_decode((string) $fcheckBadOut, true);
reg_assert('f_check 无效 hdata 返回 JSON status=error', is_array($fcBadJson) && ($fcBadJson['status'] ?? '') === 'error', (string) $fcheckBadOut);

// ── 11. 二次风控 cloakjs / RouteResolver ────────────────────────
echo "[11] 二次风控 (cloakjs / RouteResolver)\n";

$riskCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_risk_route_fixture.php') . ' 2>&1';
exec($riskCmd, $riskOut, $riskCode);
$riskResult = json_decode(implode("\n", $riskOut), true);
reg_assert(
    'RouteResolver 风险系数>0 输出二次风控页',
    ($riskResult['risk_page'] ?? false) === true && ($riskResult['has_fatal'] ?? true) === false,
    json_encode($riskResult, JSON_UNESCAPED_UNICODE)
);
reg_assert('二次风控页嵌入访客 IP', ($riskResult['has_ip'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控页嵌入 log_id', ($riskResult['has_log_id'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控页含 f_check.php AJAX', ($riskResult['has_f_check'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('visit_to_3 缺省时自动初始化', ($riskResult['visit_to_3_was_unset'] ?? false) && ($riskResult['visit_to_3_init'] ?? '') === '1', json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控首次写库为 wait 非 true', ($riskResult['log_result'] ?? '') === 'wait', json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('cloakjs WebGL extension 空值防护', ($riskResult['webgl_null_guard'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));

$cloakJsModeOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloakjs_mode.php') . ' 2>/dev/null');
$cloakJsModeJson = json_decode((string) $cloakJsModeOut, true);
reg_assert('cloakjs 基础/加强模式输出', ($cloakJsModeJson['ok'] ?? false) === true, (string) $cloakJsModeOut);

$v_infoStub       = new stdClass();
$v_infoStub->v_ip = '198.51.100.7';
$v_infoStub->v_country = 'CA';
$log_idStub       = 778899;
ob_start();
extract(['v_info' => $v_infoStub, 'log_id' => $log_idStub], EXTR_SKIP);
include $root . '/cloakjs.php';
$cloakJsOut = ob_get_clean();
reg_assert('cloakjs 独立渲染含 IP', strpos($cloakJsOut, '198.51.100.7') !== false);
reg_assert('cloakjs 独立渲染含 log_id', strpos($cloakJsOut, '778899') !== false);
reg_assert('cloakjs 默认模式不含 WebGPU 采集', strpos($cloakJsOut, 'getWebGPUFingerprint') === false);
reg_assert('cloakjs 独立渲染无 PHP Fatal', stripos($cloakJsOut, 'Fatal error') === false);

$ntVerifyCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_fcheck_nt.php') . ' 2>/dev/null';
$ntVerifyOut = shell_exec($ntVerifyCmd);
$ntVerifyJson = json_decode((string) $ntVerifyOut, true);
reg_assert('f_check nt.txt 畸形行跳过且 fp 索引合法', ($ntVerifyJson['ok'] ?? false) === true, (string) $ntVerifyOut);

$riskCacheOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_risk_cache.php') . ' 2>/dev/null');
$riskCacheJson = json_decode((string) $riskCacheOut, true);
reg_assert('二次风控 Session 缓存与 CORS', ($riskCacheJson['ok'] ?? false) === true, (string) $riskCacheOut);

$judgmentCacheOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_client_judgment_cache.php') . ' 2>/dev/null');
$judgmentCacheJson = json_decode((string) $judgmentCacheOut, true);
reg_assert('客户端 Session 统一缓存策略', ($judgmentCacheJson['ok'] ?? false) === true, (string) $judgmentCacheOut);

$datalistHelperOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_datalist_helper.php') . ' 2>/dev/null');
$datalistHelperJson = json_decode((string) $datalistHelperOut, true);
reg_assert('日志列表 VisitorLogListHelper', ($datalistHelperJson['ok'] ?? false) === true, (string) $datalistHelperOut);

$riskSessionOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_risk_session.php') . ' 2>/dev/null');
$riskSessionJson = json_decode((string) $riskSessionOut, true);
reg_assert('二次风控 Session 防重复写库', ($riskSessionJson['ok'] ?? false) === true, (string) $riskSessionOut);

$judgmentPriOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_judgment_priority.php') . ' 2>/dev/null');
$judgmentPriJson = json_decode((string) $judgmentPriOut, true);
reg_assert('判定优先级顺序', ($judgmentPriJson['ok'] ?? false) === true, (string) $judgmentPriOut);

$visitLogOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_visit_log_lifecycle.php') . ' 2>/dev/null');
$visitLogJson = json_decode((string) $visitLogOut, true);
reg_assert('Visit 生命周期与 reason 规范化', ($visitLogJson['ok'] ?? false) === true, (string) $visitLogOut);

$waitLogOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_wait_log_write.php') . ' 2>/dev/null');
$waitLogJson = json_decode((string) $waitLogOut, true);
reg_assert('二次风控 wait 日志同步完整落库', ($waitLogJson['ok'] ?? false) === true, (string) $waitLogOut);

// ── 12. ConfigLoader / 宝塔部署文件 ───────────────────────────
echo "[12] ConfigLoader 与宝塔部署\n";
require_once $root . '/config/ConfigLoader.php';
reg_assert('ConfigLoader::sanitizeConfigName', ConfigLoader::sanitizeConfigName('ab.php') === 'ab');
reg_assert('ConfigLoader::sanitizeConfigName 非法回退 index', ConfigLoader::sanitizeConfigName('../x') === 'index');

$cfgLoaderOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_config_loader.php') . ' 2>/dev/null');
$cfgLoaderJson = json_decode((string) $cfgLoaderOut, true);
reg_assert('DomainRepository 规范化', ($cfgLoaderJson['tests']['normalize_strips_www'] ?? false) === true, (string) $cfgLoaderOut);

$btNginxOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_baota_nginx.php') . ' 2>/dev/null');
$btNginxJson = json_decode((string) $btNginxOut, true);
reg_assert('宝塔 deploy 模板文件齐全', ($btNginxJson['ok'] ?? false) === true, (string) $btNginxOut);

$cfDomainOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloudflare_domain.php') . ' 2>/dev/null');
$cfDomainJson = json_decode((string) $cfDomainOut, true);
reg_assert('Cloudflare 域名自动化 Mock 流转', ($cfDomainJson['ok'] ?? false) === true, (string) $cfDomainOut);

$countryAllowOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_country_allowlist.php') . ' 2>/dev/null');
$countryAllowJson = json_decode((string) $countryAllowOut, true);
reg_assert('国家条件允许列表与 GeoIP 开关', ($countryAllowJson['ok'] ?? false) === true, (string) $countryAllowOut);

$geoOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_geoip.php') . ' 2>/dev/null');
$geoJson = json_decode((string) $geoOut, true);
if (($geoJson['tests']['db_file_exists'] ?? false) === true) {
    reg_assert('GeoIP 本地国家库', ($geoJson['ok'] ?? false) === true, (string) $geoOut);
} else {
    reg_skip('GeoIP 本地国家库', 'mmdb 未安装，运行 php tools/setup_geoip_db.php');
}

// ── 9. 数据库连通（可选） ─────────────────────────────────────
echo "[9] 数据库\n";
if (defined('DB_NAME')) {
    $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
    if ($conn->connect_error) {
        reg_skip('MySQL 连接', $conn->connect_error);
    } else {
        reg_assert('MySQL 连接', true);
        require_once $root . '/lib/Cloak/VisitorLogSchema.php';
        VisitorLogSchema::ensureColumns($conn);
        $fpHdataCol = $conn->query("SHOW COLUMNS FROM `visitor_logs` LIKE 'fp_hdata'");
        reg_assert('visitor_logs.fp_hdata 列存在', $fpHdataCol && $fpHdataCol->num_rows > 0);
        require_once $root . '/lib/Cloak/VisitorLogWorker.php';
        $prev = error_reporting(E_ALL);
        $GLOBALS['__cloak_dbg_run'] = 'regression-db-test';

        $asyncOn = defined('VISITOR_LOG_ASYNC') && strtoupper(VISITOR_LOG_ASYNC) === 'ON';
        $sampleLogs = [
            'campagin_id'      => 'reg_test',
            'visit_md5_code'   => 'reg_async_' . substr(md5((string) microtime(true)), 0, 16),
            'site_name'        => 'reg.test',
            'customers_ip'     => '127.0.0.1',
            'country'          => null,
            'result'           => 'false',
            'reason'           => 'regression',
            'v_referer'        => null,
            'Client'           => 'PC',
            'v_Browser'        => null,
            'v_PageURL'        => '/',
            'accept_language'  => null,
            'fp_url'           => null,
            'device'           => null,
        ];

        $logId = @write_log_db($sampleLogs);
        if ($asyncOn) {
            VisitorLogWorker::processBatch(20);
            if ($logId === null) {
                $md5Esc = $conn->real_escape_string($sampleLogs['visit_md5_code']);
                $res = $conn->query("SELECT `id` FROM `visitor_logs` WHERE `visit_md5_code`='{$md5Esc}' ORDER BY `id` DESC LIMIT 1");
                $row = $res ? $res->fetch_assoc() : null;
                $logId = $row ? (int) $row['id'] : null;
            }
            reg_assert('write_log_db 异步模式 worker 落库', $logId !== null && (int) $logId > 0);
        } else {
            reg_assert('write_log_db 含 null 字段可正常执行', $logId !== null && (int) $logId > 0);
        }

        $waitId = @write_log_db(array_merge($sampleLogs, [
            'visit_md5_code' => 'reg_wait_' . substr(md5((string) microtime(true)), 0, 16),
            'result'         => 'wait',
            'reason'         => '二次风控检测中',
            'fp_url'         => '',
        ]));
        reg_assert('write_log_db wait 路径返回 log_id', $waitId !== null && (int) $waitId > 0);
        if ($asyncOn) {
            VisitorLogWorker::processBatch(20);
        }

        error_reporting($prev);
        if ($logId) {
            $conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $logId);
        }
        if ($waitId) {
            $conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $waitId);
        }

        $GLOBALS['__cloak_force_sync_log'] = true;
        $simLogId = @write_log_db(array_merge($sampleLogs, [
            'visit_md5_code'    => 'reg_sim_' . substr(md5((string) microtime(true)), 0, 12),
            'is_simulation'     => 1,
            'sim_source_log_id' => 99999,
            'reason'            => VisitorSimulationRunner::tagReason('regression'),
        ]));
        unset($GLOBALS['__cloak_force_sync_log']);
        reg_assert('模拟日志 is_simulation 写入', $simLogId !== null && (int) $simLogId > 0);
        if ($simLogId) {
            $chk = $conn->query('SELECT is_simulation, sim_source_log_id FROM visitor_logs WHERE id=' . (int) $simLogId);
            $simRow = $chk ? $chk->fetch_assoc() : null;
            reg_assert('模拟列 is_simulation=1', $simRow && (int) $simRow['is_simulation'] === 1);
            reg_assert('模拟列 sim_source_log_id', $simRow && (int) $simRow['sim_source_log_id'] === 99999);
            $conn->query('DELETE FROM visitor_logs WHERE id=' . (int) $simLogId);
        }

        $conn->close();
    }
} else {
    reg_skip('MySQL 连接', 'cong.php 未定义 DB_NAME');
}

// ── 10. DEBUG 组件 ────────────────────────────────────────────
echo "[10] DEBUG 组件\n";
reg_assert('CloakDebugPage 可初始化', class_exists('CloakDebugPage', false));
CloakDebugPage::init();
CloakDebugPage::stepAt(__LINE__, 'regression', 'info', 'ok');
reg_assert('CloakDebugPage::stepAt 可调用', true);

// ── 11. 模拟测试组件 ─────────────────────────────────────────
echo "[11] 模拟测试组件\n";
require_once $root . '/lib/Cloak/VisitorSimulationRunner.php';
reg_assert('FCheckHandler 已加载', class_exists('FCheckHandler', false));
reg_assert('SimulationRouteResolver 已加载', class_exists('SimulationRouteResolver', false));
reg_assert('VisitorSimulationRunner 已加载', class_exists('VisitorSimulationRunner', false));
$tagged = VisitorSimulationRunner::tagReason('国家不符');
reg_assert('VisitorSimulationRunner::tagReason 前缀', strpos($tagged, '[模拟测试]') === 0, $tagged);
$badPrefill = VisitorSimulationRunner::prefillFromLog(0);
reg_assert('prefillFromLog(0) 返回失败', empty($badPrefill['ok']));

// ── 汇总 ──────────────────────────────────────────────────────
echo "\n=== 汇总 ===\n";
echo "通过: {$passed}  失败: {$failed}  跳过: {$skipped}\n";

if ($failed > 0) {
    echo "\n回归未通过，请修复上述失败项。\n";
    exit(1);
}

echo "\n全部回归通过。\n";
exit(0);
