<?php
declare(strict_types=1);

// Isolated HTTP GET tests. Business tables are read-only; only a short File-cache token is written.
$phase = (string)($argv[1] ?? '');
$lane = (string)($argv[2] ?? '');
$marker = (string)($argv[3] ?? '');
if (!in_array($phase, ['--run', '--audit-token'], true)
    || !in_array($lane, ['B', 'C'], true)
    || !preg_match('/^CDXSR[0-9A-F]{12}$/', $marker)) {
    fwrite(STDERR, "DENIED: exact phase/lane/marker required\n");
    exit(78);
}
$reviewed = (string)getenv('CDX_SERVICE_SPEC_REVIEWED_SHA256');
if ((string)getenv('CDX_SERVICE_SPEC_APPROVAL') !== 'SERVICE_SPEC_READ_V1'
    || !preg_match('/^[0-9a-f]{64}$/', $reviewed)
    || ($lane === 'B' && hash_file('sha256', __FILE__) !== $reviewed)
    || ($lane === 'C' && (string)getenv('CDX_SERVICE_SPEC_SOURCE_CHECKED') !== '1')) {
    fwrite(STDERR, "DENIED: reviewed source/approval gate required\n");
    exit(78);
}
$stage = 'start';
set_exception_handler(static function (Throwable $e) use (&$stage): void {
    fwrite(STDERR, 'SERVICE_SPEC_STOP stage=' . $stage . ' class=' . get_class($e) . "\n");
    exit(3);
});
$root = $lane === 'B' ? '/private/tmp/zhct-http-ai-macos.x2CCZX'
    : '/tmp/zhct-ai-http-20260929.3XDj3w';
$pid = $lane === 'B' ? 13127 : 53772;
$port = $lane === 'B' ? 18101 : 18102;
$base = 'http://127.0.0.1:' . $port;
$wanted = [
    'app/store/controller/goods/Service.php' => '319b8e191f6f126979d6d5a6b77d4dba4dda2c8b39c0fff403f00b0c24b9b15d',
    'app/store/controller/goods/Spec.php' => '6138dfe6193274b871cff2fd20d350308f49b81346872d609ce06eff961e580c',
    'app/store/model/goods/Service.php' => '7449f17affe3bf7b477cefa010c5911e17b52edcd42f3839572a6f46fe67d3a7',
    'app/common/model/GoodsSpecRel.php' => '58c1ea08bb58963f43532b11dd4c0a008f1aa33f97bb553ba9361640c7b05f01',
];
foreach ($wanted as $file => $sha) {
    if (!is_file($root . '/' . $file) || hash_file('sha256', $root . '/' . $file) !== $sha) {
        throw new RuntimeException('Worker source drift');
    }
}
if ($lane === 'B') {
    $cfg = parse_ini_file('/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local', true, INI_SCANNER_RAW)['DATABASE'] ?? null;
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    $owner = trim((string)shell_exec('/usr/sbin/lsof -nP -t -iTCP:' . $port . ' -sTCP:LISTEN 2>/dev/null'));
    if (!is_array($cfg) || strtolower((string)($cfg['TYPE'] ?? '')) !== 'mysql'
        || (string)($cfg['HOSTPORT'] ?? '') !== '3306' || $owner !== (string)$pid
        || strpos($ps, 'POC_DIALECT=mysql') === false
        || strpos($ps, 'POC_EXTERNAL_STUBS=1') === false) {
        throw new RuntimeException('B worker/config drift');
    }
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
              'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $from => $to) {
        $value = (string)($cfg[$from] ?? '');
        if ($value === '' || strpos($ps, 'POC_AI_' . $to . '=' . $value) === false) {
            throw new RuntimeException('B worker differs from code local config');
        }
    }
    $connect = static function () use ($cfg): PDO {
        return new PDO('mysql:host=' . $cfg['HOSTNAME'] . ';port=' . $cfg['HOSTPORT']
            . ';dbname=' . $cfg['DATABASE'] . ';charset=utf8', $cfg['USERNAME'], $cfg['PASSWORD'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_LOWER]);
    };
    $envName = 'local';
} else {
    $env = [];
    foreach (explode("\0", (string)file_get_contents('/proc/' . $pid . '/environ')) as $item) {
        $pair = explode('=', $item, 2);
        if (count($pair) === 2) $env[$pair[0]] = $pair[1];
    }
    if (($env['POC_TARGET_ROOT'] ?? '') !== $root
        || ($env['POC_BASE_URL'] ?? '') !== $base
        || ($env['POC_DIALECT'] ?? '') !== 'dm'
        || ($env['POC_EXTERNAL_STUBS'] ?? '') !== '1'
        || ($env['POC_AI_HOST'] ?? '') !== '127.0.0.1'
        || ($env['POC_AI_PORT'] ?? '') !== '5238'
        || ($env['POC_AI_SCHEMA'] ?? '') !== 'AIZHCT_APP'
        || ($env['POC_AI_USER'] ?? '') !== 'AIZHCT_APP'
        || !in_array('dm', PDO::getAvailableDrivers(), true)) {
        throw new RuntimeException('C worker/config drift');
    }
    $connect = static function () use ($env): PDO {
        $d = new PDO('dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP',
            $env['POC_AI_USER'], $env['POC_AI_PASSWORD'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_LOWER]);
        if ($d->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn() !== 'AIZHCT_APP') {
            throw new RuntimeException('C schema drift');
        }
        return $d;
    };
    $envName = 'poc';
}
$t = static function (string $logical) use ($lane): string {
    if (!in_array($logical, ['goods', 'goods_service', 'goods_spec_rel', 'spec', 'spec_value'], true)) {
        throw new RuntimeException('Unknown table');
    }
    return 'yoshop_' . ($lane === 'B' ? $logical : strtoupper($logical));
};
$pk = ['goods' => 'GOODS_ID', 'goods_service' => 'SERVICE_ID',
    'goods_spec_rel' => 'ID', 'spec' => 'SPEC_ID', 'spec_value' => 'SPEC_VALUE_ID'];
$digest = static function (PDO $d) use ($pk, $t): array {
    $out = [];
    foreach ($pk as $logical => $column) {
        $s = $d->query('SELECT * FROM ' . $t($logical)
            . ' WHERE STORE_ID=10001 ORDER BY ' . $column);
        $h = hash_init('sha256');
        $n = 0;
        while (($row = $s->fetch(PDO::FETCH_NUM)) !== false) {
            hash_update($h, serialize($row));
            if (++$n > 30000) throw new RuntimeException('Source cap exceeded');
        }
        $out[$logical] = ['count' => $n, 'sha256' => hash_final($h)];
    }
    return $out;
};
$db = $connect();
$stage = 'db-preflight';
$before = $digest($db);
$services = $db->query('SELECT SERVICE_ID FROM ' . $t('goods_service')
    . ' WHERE STORE_ID=10001 AND IS_DELETE=0 ORDER BY SERVICE_ID')->fetchAll(PDO::FETCH_COLUMN);
$serviceRows = $db->query('SELECT SERVICE_ID,NAME,SUMMARY,STATUS,SORT,IS_DEFAULT FROM '
    . $t('goods_service')
    . ' WHERE STORE_ID=10001 AND IS_DELETE=0 ORDER BY SERVICE_ID')->fetchAll(PDO::FETCH_ASSOC);
$goodsWithSpecs = $db->query('SELECT DISTINCT r.GOODS_ID FROM ' . $t('goods_spec_rel') . ' r'
    . ' JOIN ' . $t('goods') . ' g ON g.GOODS_ID=r.GOODS_ID AND g.STORE_ID=10001 AND g.IS_DELETE=0'
    . ' JOIN ' . $t('spec') . ' s ON s.SPEC_ID=r.SPEC_ID'
    . ' JOIN ' . $t('spec_value') . ' v ON v.SPEC_VALUE_ID=r.SPEC_VALUE_ID'
    . ' WHERE r.STORE_ID=10001 ORDER BY r.GOODS_ID')->fetchAll(PDO::FETCH_COLUMN);
if (count($services) !== 3 || array_map('intval', $services) !== [10001, 10002, 10003]
    || ($lane === 'B' && count($goodsWithSpecs) < 1)
    || ($lane === 'C' && count($goodsWithSpecs) !== 0)) {
    throw new RuntimeException('Natural source gate changed');
}
define('TP_ENV', $envName);
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require_once $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName($envName)->initialize();
if (config('cache.default') !== 'file') throw new RuntimeException('Private File cache absent');
$tokenDir = sys_get_temp_dir() . '/cdx-goods-service-spec-'
    . strtolower($lane) . '-' . strtolower($marker);
$journal = $tokenDir . '/journal.json';
if ($phase === '--audit-token') {
    if (!is_file($journal)) throw new RuntimeException('Token journal absent');
    $j = json_decode((string)file_get_contents($journal), true);
    if (!is_array($j) || ($j['lane'] ?? '') !== $lane
        || ($j['marker'] ?? '') !== $marker
        || !preg_match('/^CDXSRREAD[0-9A-F]{32}$/', (string)($j['token'] ?? ''))
        || !is_array($j['source_before'] ?? null)) {
        throw new RuntimeException('Token journal invalid');
    }
    $tokenZero = !\think\facade\Cache::has($j['token']);
    $same = $digest($connect()) === $j['source_before'];
    if (!$tokenZero || !$same) throw new RuntimeException('Independent token/source audit failed');
    if (!unlink($journal) || !rmdir($tokenDir)
        || file_exists($journal) || is_dir($tokenDir)) {
        throw new RuntimeException('Exact token journal removal failed');
    }
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'token_zero' => true, 'five_source_tables_unchanged' => true,
        'journal_absent' => true, 'business_dml' => 0], JSON_UNESCAPED_SLASHES), "\n";
    exit(0);
}
if (file_exists($tokenDir) || !mkdir($tokenDir, 0700) || !chmod($tokenDir, 0700)) {
    throw new RuntimeException('Token journal not pristine');
}
$token = 'CDXSRREAD' . strtoupper(bin2hex(random_bytes(16)));
$j = ['lane' => $lane, 'marker' => $marker,
    'token' => $token, 'source_before' => $before, 'phase' => 'before_token'];
$save = static function () use (&$j, $journal): void {
    $body = json_encode($j, JSON_UNESCAPED_SLASHES);
    $tmp = $journal . '.tmp';
    if (!is_string($body) || file_put_contents($tmp, $body, LOCK_EX) !== strlen($body)
        || !chmod($tmp, 0600) || !rename($tmp, $journal)) {
        throw new RuntimeException('Token journal write failed');
    }
};
$save();
$shape = null;
$shape = static function ($v) use (&$shape) {
    if (is_array($v)) {
        if (array_values($v) === $v) {
            return ['@array' => count($v), '@items' => array_map($shape, $v)];
        }
        $out = [];
        foreach ($v as $key => $item) $out[(string)$key] = $shape($item);
        ksort($out);
        return $out;
    }
    return gettype($v);
};
$request = static function (string $path, array $query, ?string $auth) use ($base): array {
    $url = $base . '/index.php?s=/store/' . $path;
    if ($query) $url .= '&' . http_build_query($query);
    $header = $auth === null ? '' : 'Access-Token: ' . $auth . "\r\n";
    $ctx = stream_context_create(['http' => ['method' => 'GET',
        'header' => $header, 'timeout' => 20, 'ignore_errors' => true,
        'follow_location' => 0]]);
    $start = microtime(true);
    $h = @fopen($url, 'rb', false, $ctx);
    if ($h === false) return ['complete' => false, 'http' => 0, 'json' => null];
    $raw = stream_get_contents($h, 1048577);
    $meta = stream_get_meta_data($h);
    $eof = feof($h);
    fclose($h);
    $lines = $meta['wrapper_data'] ?? [];
    preg_match('/^HTTP\/\S+\s+(\d{3})(?:\s|$)/', (string)($lines[0] ?? ''), $m);
    $http = isset($m[1]) ? (int)$m[1] : 0;
    $lengthOk = true;
    foreach ($lines as $line) {
        if (preg_match('/^Content-Length:\s*(\d+)\s*$/i', (string)$line, $n)) {
            $lengthOk = is_string($raw) && strlen($raw) === (int)$n[1];
        }
    }
    $json = is_string($raw) && strlen($raw) <= 1048576 ? json_decode($raw, true) : null;
    $complete = $http > 0 && is_string($raw) && strlen($raw) <= 1048576
        && $eof && !($meta['timed_out'] ?? false) && $lengthOk
        && is_array($json) && json_last_error() === JSON_ERROR_NONE;
    return ['complete' => $complete, 'http' => $http,
        'json' => $complete ? $json : null,
        'elapsed_ms' => (int)round((microtime(true) - $start) * 1000)];
};
$out = ['lane' => $lane, 'phase' => $phase, 'marker' => $marker,
    'routes' => [], 'token_deleted' => false,
    'five_source_tables_unchanged' => false, 'business_dml' => 0];
$issued = false;
try {
    $stage = 'token';
    if (!\think\facade\Cache::set($token, ['user' => [
            'store_user_id' => 2147480010, 'store_id' => 10001,
            'user_name' => $marker, 'real_name' => $marker,
            'is_super' => 1], 'store_id' => 10001,
            'is_login' => true], 300)
        || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Short File token unavailable');
    }
    $issued = true;
    $j['phase'] = 'token_issued'; $save();
    foreach ([
        ['service_list', 'goods.Service/list', ['page' => 1, 'pageSize' => 10]],
        ['service_all', 'goods.Service/all', []],
        ['spec_list', 'goods.Spec/list', ['goodsId' => $lane === 'B' ? (int)$goodsWithSpecs[0] : 10001]],
    ] as [$key, $path, $query]) {
        $stage = $key;
        $unauth = $request($path, $query, null);
        if (!$unauth['complete']
            || (int)($unauth['json']['status'] ?? 200) === 200) {
            throw new RuntimeException('Unauthenticated request not conclusively rejected');
        }
        $r = $request($path, $query, $token);
        if (!$r['complete'] || $r['http'] !== 200
            || (int)($r['json']['status'] ?? -1) !== 200) {
            throw new RuntimeException('HTTP business result failed');
        }
        $list = $r['json']['data']['list'] ?? null;
        if (!is_array($list)) throw new RuntimeException('HTTP list absent');
        $items = $key === 'service_list' ? ($list['data'] ?? null) : $list;
        if (!is_array($items)) throw new RuntimeException('HTTP items absent');
        if ($key !== 'spec_list') {
            $ids = array_map(static function (array $x): int {
                return (int)($x['service_id'] ?? 0);
            }, $items);
            sort($ids);
            $expected = array_map('intval', $services);
            if ($ids !== $expected) throw new RuntimeException('HTTP service IDs differ from original table');
            foreach ($items as $item) {
                $matched = null;
                foreach ($serviceRows as $source) {
                    if ((int)$source['service_id'] === (int)$item['service_id']) {
                        $matched = $source;
                        break;
                    }
                }
                if (!is_array($matched)
                    || (string)$item['name'] !== (string)$matched['name']
                    || (string)$item['summary'] !== (string)$matched['summary']
                    || (int)$item['status'] !== (int)$matched['status']
                    || (int)$item['sort'] !== (int)$matched['sort']
                    || (int)$item['is_default'] !== (int)$matched['is_default']) {
                    throw new RuntimeException('HTTP service fields differ from original table');
                }
            }
        } elseif ($lane === 'B' && count($items) < 1) {
            throw new RuntimeException('B natural specs unexpectedly empty');
        } elseif ($lane === 'C' && count($items) !== 0) {
            throw new RuntimeException('C empty-branch specs unexpectedly populated');
        }
        if ($key === 'spec_list' && $lane === 'B') {
            $s = $db->prepare('SELECT r.SPEC_ID,s.SPEC_NAME,r.SPEC_VALUE_ID,v.SPEC_VALUE FROM '
                . $t('goods_spec_rel') . ' r'
                . ' JOIN ' . $t('spec') . ' s ON s.SPEC_ID=r.SPEC_ID'
                . ' JOIN ' . $t('spec_value') . ' v ON v.SPEC_VALUE_ID=r.SPEC_VALUE_ID'
                . ' WHERE r.STORE_ID=10001 AND r.GOODS_ID=? ORDER BY r.ID');
            $s->execute([(int)$query['goodsId']]);
            $sourceRelations = $s->fetchAll(PDO::FETCH_ASSOC);
            $actualRelations = [];
            foreach ($items as $group) {
                foreach (($group['valueList'] ?? []) as $value) {
                    $actualRelations[] = implode('|', [(int)$group['spec_id'],
                        (string)$group['spec_name'], (int)$value['spec_value_id'],
                        (string)$value['spec_value']]);
                }
            }
            $expectedRelations = [];
            foreach ($sourceRelations as $relation) {
                $expectedRelations[] = implode('|', [(int)$relation['spec_id'],
                    (string)$relation['spec_name'], (int)$relation['spec_value_id'],
                    (string)$relation['spec_value']]);
            }
            sort($actualRelations);
            sort($expectedRelations);
            if (!$expectedRelations || $actualRelations !== $expectedRelations) {
                throw new RuntimeException('HTTP spec groups/values differ from original tables');
            }
        }
        $out['routes'][$key] = [
            'http' => $r['http'], 'status' => (int)$r['json']['status'],
            'item_count' => count($items), 'elapsed_ms' => $r['elapsed_ms'],
            'source_fields_match' => true,
            'unauth_rejected' => true,
            'unauth_status' => (int)$unauth['json']['status'],
            'unauth_full_json_key_type_shape' => $shape($unauth['json']),
            'full_json_key_type_shape' => $shape($r['json']),
        ];
    }
    $stage = 'source-readback';
    $out['five_source_tables_unchanged'] = $digest($connect()) === $before;
    if (!$out['five_source_tables_unchanged']) {
        throw new RuntimeException('Business source changed during GET tests');
    }
    $j['phase'] = 'completed'; $save();
} catch (Throwable $e) {
    $out['failure_stage'] = $stage;
    $out['failure_class'] = get_class($e);
} finally {
    if ($issued) {
        \think\facade\Cache::delete($token);
        $out['token_deleted'] = !\think\facade\Cache::has($token);
    }
}
$out['pass'] = !isset($out['failure_class']) && $out['token_deleted']
    && $out['five_source_tables_unchanged'] && count($out['routes']) === 3;
echo json_encode($out, JSON_UNESCAPED_SLASHES), "\n";
exit($out['pass'] ? 0 : 3);
