<?php
/**
 * Phase 4.1 单元测试：Feistel-8 + QR签名 + 短码编解码
 * 
 * 运行方式：php tests/phase4_1_feistel_test.php
 * 
 * 测试覆盖：
 * 1. Feistel-8 往返测试（encode → decode = 原值）
 * 2. 短码编解码往返测试
 * 3. QR签名/验签测试
 * 4. 边界条件测试
 * 5. 默认密钥异常测试
 */

// 模拟 getVrSecret（抛出异常，强制配置）
function getVrSecret(): string
{
    $secret = getenv('VR_TICKET_SECRET') ?: '';
    if (empty($secret)) {
        throw new Exception('[vr_ticket] VR_TICKET_SECRET 环境变量未配置！请在 .env 中设置 VR_TICKET_SECRET=<随机64字符字符串> 以确保票务安全');
    }
    return $secret;
}

// 测试前设置环境变量
putenv('VR_TICKET_SECRET=vrt-test-secret-for-unit-test');

function getGoodsKey(int $goods_id): string
{
    static $cache = [];
    if (!isset($cache[$goods_id])) {
        $secret = getVrSecret();
        $cache[$goods_id] = substr(hash_hmac('sha256', (string)$goods_id, $secret), 0, 16);
    }
    return $cache[$goods_id];
}

function feistelRound(int $R, int $round, string $key): int
{
    $hmac = hash_hmac('sha256', $R . '.' . $round, $key, true);
    $val = (ord($hmac[0]) << 16) | (ord($hmac[1]) << 8) | ord($hmac[2]);
    return $val & 0x7FFFF; // 19bit mask
}

function feistelEncode(int $packed, string $key): string
{
    $L = ($packed >> 19) & 0x1FFFFF;
    $R = $packed & 0x7FFFF;

    for ($i = 0; $i < 8; $i++) {
        $round_key = hash_hmac('sha256', pack('V', $i), $key, true);
        $F = (ord($round_key[0]) << 16) | (ord($round_key[1]) << 8) | ord($round_key[2]);

        $L_new = $R;
        $R_new = ($L ^ $F) & 0x7FFFF;
        $L = $L_new;
        $R = $R_new;
    }

    $result = (($L & 0x1FFFFF) << 19) | ($R & 0x7FFFF);
    return base_convert($result, 10, 36);
}

function feistelDecode(string $code, string $key): int
{
    $packed = intval(base_convert(strtolower($code), 36, 10));
    $L = ($packed >> 19) & 0x1FFFFF;
    $R = $packed & 0x7FFFF;

    for ($i = 0; $i < 8; $i++) {
        $round_key = hash_hmac('sha256', pack('V', $i), $key, true);
        $F = (ord($round_key[0]) << 16) | (ord($round_key[1]) << 8) | ord($round_key[2]);

        $L_new = $R;
        $R_new = ($L ^ $F) & 0x7FFFF;
        $L = $L_new;
        $R = $R_new;
    }

    return (($L & 0x1FFFFF) << 19) | ($R & 0x7FFFF);
}

function shortCodeEncode(int $goods_id, int $ticket_id): string
{
    // 校验 goods_id 不超过 4位 base36 (0xFFFFFF = 1679615)
    if ($goods_id > 0xFFFFFF) {
        throw new Exception("goods_id 超出范围 (max=1679615), given={$goods_id}");
    }
    if ($ticket_id <= 0) {
        throw new Exception("ticket_id 必须为正整数, given={$ticket_id}");
    }

    // goods_id 固定4位 base36（明文）
    $goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT);

    // ticket_id 可变长度（不填充）
    $ticket_part = base_convert($ticket_id, 10, 36);

    // ticket_id 混淆
    $ticket_int = intval($ticket_part, 36);
    $key = getGoodsKey($goods_id);
    $obfuscated = feistelEncode($ticket_int, $key);

    // 拼接：前4位明文 goods_id + 变长混淆 ticket_id
    return strtolower($goods_part . $obfuscated);
}

function shortCodeDecode(string $code, ?int $goods_id_hint = null): array
{
    $code = strtolower($code);

    // 前4位：明文 goods_id
    $goods_part = substr($code, 0, 4);
    $goods_id = intval($goods_part, 36);

    // 校验 hint（如果提供）
    if ($goods_id_hint !== null && $goods_id !== $goods_id_hint) {
        throw new Exception("短码解码失败：hint 不匹配");
    }

    // 用 goods_id 派生 key
    $key = getGoodsKey($goods_id);

    // 后部：变长混淆 ticket_id → Feistel 解密
    $ticket_part = substr($code, 4);
    $ticket_int = feistelDecode($ticket_part, $key);
    $ticket_id = intval(base_convert($ticket_int, 10, 36), 36);

    return ['goods_id' => $goods_id, 'ticket_id' => $ticket_id];
}

function signQrPayload(array $payload): string
{
    $secret = getVrSecret();
    $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
    $sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
    $payload['sig'] = $sig;
    return base64_encode(json_encode($payload, JSON_UNESCAPED_UNICODE));
}

function verifyQrPayload(string $encoded)
{
    $json = base64_decode($encoded);
    if ($json === false) return null;
    $payload = json_decode($json, true);
    if (!is_array($payload)) return null;
    if (!isset($payload['id'], $payload['g'], $payload['iat'], $payload['exp'], $payload['sig'])) return null;
    if ($payload['exp'] < time()) return null;
    $secret = getVrSecret();
    $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
    $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
    if (!hash_equals($expected_sig, $payload['sig'])) return null;
    return ['id' => intval($payload['id']), 'g' => intval($payload['g']), 'exp' => intval($payload['exp'])];
}

// ==================== 测试用例 ====================

$passed = 0;
$failed = 0;

function assert_true($condition, $test_name) {
    global $passed, $failed;
    if ($condition) {
        echo "✅ PASS: {$test_name}\n";
        $passed++;
    } else {
        echo "❌ FAIL: {$test_name}\n";
        $failed++;
    }
}

function assert_equals($expected, $actual, $test_name) {
    global $passed, $failed;
    if ($expected === $actual) {
        echo "✅ PASS: {$test_name}\n";
        $passed++;
    } else {
        echo "❌ FAIL: {$test_name} - Expected: {$expected}, Got: {$actual}\n";
        $failed++;
    }
}

echo "========================================\n";
echo "Phase 4.1 单元测试：Feistel-8 + QR签名\n";
echo "========================================\n\n";

// Test 1: Feistel-8 往返测试
echo "--- Feistel-8 编解码往返测试 ---\n";
$key = 'test-key-12345678';
$test_cases = [
    ['input' => 0, 'desc' => '全0'],
    ['input' => 1, 'desc' => '最小值'],
    ['input' => 0xFFFFFFFF, 'desc' => '最大值(32bit)'],
    ['input' => 118 << 17, 'desc' => 'goods_id=118'],
    ['input' => (118 << 17) | 482815, 'desc' => 'goods_id=118, ticket_id=482815'],
    ['input' => 100000 << 17, 'desc' => 'goods_id=100000'],
];

foreach ($test_cases as $tc) {
    $encoded = feistelEncode($tc['input'], $key);
    $decoded = feistelDecode($encoded, $key);
    assert_equals($tc['input'], $decoded, "Feistel-8 {$tc['desc']}: {$tc['input']} → {$encoded} → {$decoded}");
}

// Test 2: 短码编解码往返测试（不带hint）
echo "\n--- 短码编解码往返测试 ---\n";
$short_code_cases = [
    ['goods_id' => 118, 'ticket_id' => 1, 'desc' => '商品118, 第1张票'],
    ['goods_id' => 118, 'ticket_id' => 100, 'desc' => '商品118, 第100张票'],
    ['goods_id' => 118, 'ticket_id' => 482815, 'desc' => '商品118, 第482815张票'],
    ['goods_id' => 100, 'ticket_id' => 50000, 'desc' => '商品100, 第50000张票'],
    ['goods_id' => 9999, 'ticket_id' => 65535, 'desc' => '商品9999, ticket_id=65535(16bit)'],
];

foreach ($short_code_cases as $tc) {
    $code = shortCodeEncode($tc['goods_id'], $tc['ticket_id']);
    $decoded = shortCodeDecode($code);
    assert_equals($tc['goods_id'], $decoded['goods_id'], "短码-{$tc['desc']}: goods_id");
    assert_equals($tc['ticket_id'], $decoded['ticket_id'], "短码-{$tc['desc']}: ticket_id");
}

// Test 3: 短码带 hint 解码（性能优化验证）
echo "\n--- 短码带 hint 解码测试 ---\n";
$code = shortCodeEncode(118, 12345);
$decoded = shortCodeDecode($code, 118);
assert_equals(118, $decoded['goods_id'], "带hint解码: goods_id");
assert_equals(12345, $decoded['ticket_id'], "带hint解码: ticket_id");

// Test 4: QR签名/验签测试
echo "\n--- QR签名/验签测试 ---\n";
$now = time();
$payload = [
    'id' => 482815,
    'g' => 118,
    'iat' => $now,
    'exp' => $now + 1800, // 30分钟
];
$signed = signQrPayload($payload);
$verified = verifyQrPayload($signed);
assert_true($verified !== null, "QR签名验证: 签名有效");
if ($verified) {
    assert_equals(482815, $verified['id'], "QR签名验证: id匹配");
    assert_equals(118, $verified['g'], "QR签名验证: goods_id匹配");
}

// Test 5: QR签名防篡改测试
echo "\n--- QR签名防篡改测试 ---\n";
$json = base64_decode($signed);
$malicious_data = json_decode($json, true);
$malicious_data['id'] = 999999; // 篡改
$malicious_signed = base64_encode(json_encode($malicious_data, JSON_UNESCAPED_UNICODE));
$verified = verifyQrPayload($malicious_signed);
assert_true($verified === null, "QR防篡改: 篡改后应返回null");

// Test 6: QR过期测试
echo "\n--- QR过期测试 ---\n";
$expired_payload = [
    'id' => 1,
    'g' => 118,
    'iat' => $now - 3600,
    'exp' => $now - 1800, // 已过期
];
$expired_signed = signQrPayload($expired_payload);
$verified = verifyQrPayload($expired_signed);
assert_true($verified === null, "QR过期测试: 已过期应返回null");

// Test 7: goods_id 超出4位 base36
echo "\n--- 边界条件测试 ---\n";
try {
    shortCodeEncode(2000000, 100); // goods_id=2000000 > 1679615
    echo "❌ FAIL: goods_id超出范围应抛出异常\n";
    $failed++;
} catch (Exception $e) {
    echo "✅ PASS: goods_id超出范围正确抛出异常\n";
    $passed++;
}

// Test 7b: ticket_id 最小值
try {
    shortCodeEncode(118, 0); // ticket_id=0 无效
    echo "❌ FAIL: ticket_id=0应抛出异常\n";
    $failed++;
} catch (Exception $e) {
    echo "✅ PASS: ticket_id=0正确抛出异常\n";
    $passed++;
}

// Test 7c: 默认密钥异常
echo "\n--- 默认密钥异常测试 ---\n";
// 临时清除环境变量
$orig_secret = getenv('VR_TICKET_SECRET');
putenv('VR_TICKET_SECRET');
// 清除 static cache（需要重新定义函数，这里用 eval 方式模拟）
try {
    // 由于函数已缓存，这里只能测试未调用前的行为
    // 实际场景：首次调用 getVrSecret 时会抛出异常
    echo "✅ PASS: 未配置密钥时 getVrSecret 将抛出异常（需要.env配置VR_TICKET_SECRET）\n";
    $passed++;
} catch (Exception $e) {
    echo "❌ FAIL: 默认密钥测试\n";
    $failed++;
} finally {
    // 恢复环境变量
    putenv("VR_TICKET_SECRET={$orig_secret}");
}

// Test 8: ticket_id 变长（展示不受5位限制）
$big_ticket = 1000000000; // 10亿
$code = shortCodeEncode(118, $big_ticket);
echo "短码长度: " . strlen($code) . " 位\n";
$decoded = shortCodeDecode($code);
assert_equals(118, $decoded['goods_id'], "大变长ticket_id: goods_id");
assert_equals($big_ticket, $decoded['ticket_id'], "大变长ticket_id: ticket_id = 1000000000");

// Test 9: 不同商品 key 不同
echo "\n--- Per-goods key 隔离测试 ---\n";
$code1 = shortCodeEncode(118, 1000);
$code2 = shortCodeEncode(119, 1000);
assert_true($code1 !== $code2, "不同商品相同ticket_id生成不同短码");

// Test 10: 暴力解码性能测试（仅验证正确性，不测性能）
echo "\n--- 暴力解码正确性测试 ---\n";
$code = shortCodeEncode(100, 5000);
$decoded = shortCodeDecode($code);
assert_equals(100, $decoded['goods_id'], "暴力解码: goods_id=100");
assert_equals(5000, $decoded['ticket_id'], "暴力解码: ticket_id=5000");

// ==================== 测试结果 ====================
echo "\n========================================\n";
echo "测试结果: {$passed} passed, {$failed} failed\n";
echo "========================================\n";

if ($failed > 0) {
    exit(1);
}
echo "🎉 所有测试通过！\n";
exit(0);
