<?php
declare(strict_types=1);
date_default_timezone_set('Asia/Shanghai');
$lane=getenv('PILOT_LANE');$root=getenv('PILOT_TARGET_ROOT');$runtime=getenv('PILOT_RUNTIME');$base=getenv('POC_BASE_URL');$dm=getenv('POC_DIALECT')==='dm';
if(!in_array($lane,['B','C'],true)||!preg_match('#^http://127\.0\.0\.1:1824[12]$#',$base))exit(78);
define('TP_ENV','poc');define('BUSINESS','aizhct');define('ENTERPRISE_NO','10001');
$loader=require $root.'/vendor/autoload.php';$overlay=getenv('TERMINAL_SOURCE_OVERLAY');
if(!is_file($overlay.'/app/terminal/controller/Enum.php'))throw new RuntimeException('Private source overlay absent');
$loader->setPsr4('app\\',$overlay.'/app');$loader->setPsr4('cores\\',$overlay.'/cores');
require $root.'/external_stubs.php';
$app=new \think\App($root);$app->setEnvName('poc');$app->setRuntimePath($runtime.'/runner/');$app->initialize();
$app->config->set(['default'=>'file','stores'=>['file'=>['type'=>'File','path'=>$runtime.'/cache/','prefix'=>'','expire'=>0]]],'cache');
$dsn=$dm?'dm:host='.getenv('POC_AI_HOST').';port='.getenv('POC_AI_PORT').';schema='.getenv('POC_AI_SCHEMA'):'mysql:host='.getenv('POC_AI_HOST').';port='.getenv('POC_AI_PORT').';dbname='.getenv('POC_AI_SCHEMA').';charset=utf8';
$pdo=new PDO($dsn,getenv('POC_AI_USER'),getenv('POC_AI_PASSWORD'),[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_CASE=>PDO::CASE_LOWER]);
$table=static function($t)use($dm){return $dm?strtoupper('yoshop_'.$t):'yoshop_'.$t;};
$q=static function($sql,$args=[])use($pdo){$s=$pdo->prepare($sql);$s->execute($args);$rows=$s->fetchAll(PDO::FETCH_ASSOC);foreach($rows as &$r)foreach($r as &$v)if(is_resource($v))$v=stream_get_contents($v);return $rows;};
$hash=static function($v){return hash('sha256',json_encode($v,JSON_UNESCAPED_UNICODE));};
$shape=static function($v)use(&$shape){if(!is_array($v))return gettype($v);if($v===[]||array_keys($v)===range(0,count($v)-1)){$a=[];foreach($v as $x)$a[json_encode($shape($x))]=true;$a=array_keys($a);sort($a);return ['items'=>$a];}$a=[];foreach($v as $k=>$x)$a[$k]=$shape($x);ksort($a);return $a;};
$tables=['config_enum','foods','foods_cate','dishes','dishes_cate','dishes_foods','dishes_unsuitable','upload_file','nation','sport'];
$snapshot=static function()use($tables,$pdo,$table,$hash){$o=[];foreach($tables as $t){$stmt=$pdo->query('SELECT * FROM '.$table($t));$rowHashes=[];while($r=$stmt->fetch(PDO::FETCH_ASSOC)){foreach($r as &$v)if(is_resource($v))$v=stream_get_contents($v);unset($v);$rowHashes[]=$hash($r);}sort($rowHashes);$o[$t]=['rows'=>count($rowHashes),'sha256'=>$hash($rowHashes)];}return $o;};
$before=$snapshot();$userRows=$q('SELECT * FROM '.$table('store_user').' WHERE STORE_ID=10001 AND IS_DELETE=0');if(!$userRows)throw new RuntimeException('No read-only authentication fixture');
$user=$userRows[0];$user['store_id']=(int)$user['store_id'];$user['store_user_id']=(int)$user['store_user_id'];
$token=bin2hex(random_bytes(24));\think\facade\Cache::set($token,['user'=>$user,'store_id'=>10001,'is_login'=>true],3600);
$boundary=json_decode(file_get_contents($base.'/__poc_boundary'),true);if(empty($boundary['external_stubs']))throw new RuntimeException('External boundary absent');
file_get_contents($base.'/terminal/dishes.Foods/add',false,stream_context_create(['http'=>['method'=>'POST','ignore_errors'=>true]]));if(strpos($http_response_header[0]??'','403')===false)throw new RuntimeException('Write boundary absent');
file_put_contents($runtime.'/ai_queries.jsonl','');chmod($runtime.'/ai_queries.jsonl',0600);
$sources=[];foreach($tables as $t){
    if(in_array($t,['dishes_foods','dishes_unsuitable','upload_file'],true))continue;
    if(in_array($t,['foods','dishes'],true)){
        $min=$q('SELECT MIN(ID) AS ID FROM '.$table($t).' WHERE STORE_ID=10001 AND IS_DELETE=0')[0]['id'];
        $name=$q('SELECT NAME FROM '.$table($t).' WHERE ID=?',[$min])[0]['name'];
        $sources[$t]=$q('SELECT * FROM '.$table($t).' WHERE STORE_ID=10001 AND IS_DELETE=0 AND NAME LIKE ?',['%'.$name.'%']);
    }else $sources[$t]=$q('SELECT * FROM '.$table($t));
}
$active=static function($t)use($sources){return array_values(array_filter($sources[$t],static function($r){return (!isset($r['store_id'])||(int)$r['store_id']===10001)&&(!isset($r['is_delete'])||(int)$r['is_delete']===0);}));};
$foods=$active('foods');$dishes=$active('dishes');usort($foods,static function($a,$b){return $a['id']<=>$b['id'];});usort($dishes,static function($a,$b){return $a['id']<=>$b['id'];});
if(!$foods||!$dishes)throw new RuntimeException('Positive catalog fixture absent');
$foodId=(int)$foods[0]['id'];$dishId=(int)$dishes[0]['id'];
$same=static function($v,$expected){if(is_numeric($v)&&is_numeric($expected))return bccomp((string)$v,(string)$expected,10)===0;return $v===$expected;};
$checkRows=static function($list,$source,$tree=false)use($same,&$checkRows){if(!is_array($list))return [false,0,0];$map=[];foreach($source as $r)$map[(string)$r['id']]=$r;$checked=0;$rows=0;foreach($list as $r){if(!is_array($r)||!isset($r['id'])||!isset($map[(string)$r['id']]))return [false,$checked,$rows];$orig=$map[(string)$r['id']];++$rows;foreach($r as $f=>$v){if($f==='children'&&$tree){[$ok,$n,$nr]=$checkRows($v,$source,true);if(!$ok)return [false,$checked,$rows];$checked+=$n;$rows+=$nr;continue;}if(!array_key_exists($f,$orig))continue;$e=$orig[$f];if(in_array($f,['create_time','update_time'],true)&&is_numeric($e))$e=(int)$e>0?date('Y-m-d H:i:s',(int)$e):'';if(in_array($f,['create_time','update_time'],true)&&is_string($e)&&preg_match('/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}$/D',$e))$e=substr($e,0,19);if(!$same($v,$e))return [false,$checked,$rows,['field'=>$f,'http_type'=>gettype($v),'source_type'=>gettype($e),'date_expected'=>in_array($f,['create_time','update_time'],true)?$e:null,'date_actual'=>in_array($f,['create_time','update_time'],true)?$v:null]];++$checked;}}return [$rows>0&&$checked>0,$checked,$rows];};
$cases=[];$bodies=[];
$request=static function($name,$path,$args,$source,$kind='list')use($base,$token,$hash,$shape,$checkRows,&$cases,&$bodies){$raw=file_get_contents($base.$path.'?'.http_build_query(['storeId'=>10001]+$args),false,stream_context_create(['http'=>['timeout'=>30,'ignore_errors'=>true,'header'=>"Platform: POC\r\nAccess-Token: ".$token."\r\nX-Pilot-Scenario: PILOT_AI_TERMINAL_".$name."\r\n"]]));preg_match('#^HTTP/\S+\s+(\d+)#',$http_response_header[0]??'',$m);$body=json_decode((string)$raw,true);$data=$body['data']??[];$list=$kind==='detail'?[$data['detail']??null]:($data['list']??[]);if($kind==='more')$list=$data['detail']??[];if($kind==='page')$list=$list['data']??[];$audit=$checkRows($list,$source,$kind==='tree');[$ok,$fields,$rows]=$audit;$cases[]=['name'=>$name,'method'=>'GET','path'=>$path,'http_status'=>(int)($m[1]??0),'business_status'=>$body['status']??null,'source_projection_equal'=>$ok,'source_fields_compared'=>$fields,'source_mismatch'=>$audit[3]??null,'positive_rows'=>$rows,'shape'=>$shape($body),'response_hash_without_trace'=>$hash(array_diff_key($body??[],['trace_id'=>true]))];$bodies[$name]=['http_status'=>(int)($m[1]??0),'body'=>$body];};
try{
foreach([1,2,3,17] as $type){$rows=array_values(array_filter($sources['config_enum'],static function($r)use($type){return (int)$r['enum_type']===$type&&(int)$r['is_delete']===0;}));$project=[];foreach($rows as $r)$project[]=['id'=>$r['enum_key']]+$r;
// Enum has enum_key, not id; handled by a dedicated independent projection below.
$path='/terminal/Enum/all';$raw=file_get_contents($base.$path.'?'.http_build_query(['enumType'=>$type,'storeId'=>10001]),false,stream_context_create(['http'=>['ignore_errors'=>true,'header'=>"Access-Token: ".$token."\r\nX-Pilot-Scenario: PILOT_AI_TERMINAL_ENUM_".$type."\r\n"]]));preg_match('#^HTTP/\S+\s+(\d+)#',$http_response_header[0]??'',$m);$body=json_decode($raw,true);$list=$body['data']['list']??[];$ok=count($list)===count($rows)&&count($list)>0;$n=0;$map=[];foreach($rows as $r)$map[(string)$r['enum_key']]=$r;foreach($list as $r){$o=$map[(string)$r['enum_key']]??[];foreach($r as $f=>$v){$ok=$ok&&array_key_exists($f,$o)&&$same($v,$o[$f]);++$n;}}$name='ENUM_'.$type;$cases[]=['name'=>$name,'method'=>'GET','path'=>$path,'http_status'=>(int)($m[1]??0),'business_status'=>$body['status']??null,'source_projection_equal'=>$ok,'source_fields_compared'=>$n,'positive_rows'=>count($list),'shape'=>$shape($body),'response_hash_without_trace'=>$hash(array_diff_key($body??[],['trace_id'=>true]))];$bodies[$name]=['http_status'=>(int)($m[1]??0),'body'=>$body];}
$request('FOODS_ALL','/terminal/dishes.Foods/all',['search'=>$foods[0]['name']],$foods);
$request('FOODS_LIST','/terminal/dishes.Foods/list',['search'=>$foods[0]['name'],'page'=>1],$foods,'page');
$request('FOODS_CATE','/terminal/dishes.Foods/cate',[],$sources['foods_cate'],'tree');
$request('FOODS_DETAIL','/terminal/dishes.Foods/detail',['id'=>$foodId],$foods,'detail');
$request('DISHES_ALL','/terminal/dishes.Dishes/all',['ids'=>(string)$dishId],$dishes);
$request('DISHES_LIST','/terminal/dishes.Dishes/list',['search'=>$dishes[0]['name'],'page'=>1],$dishes,'page');
$request('DISHES_CATE','/terminal/dishes.Dishes/cate',[],$sources['dishes_cate'],'tree');
$request('DISHES_DETAIL','/terminal/dishes.Dishes/detail',['id'=>$dishId],$dishes,'detail');
$request('DISHES_MORE','/terminal/dishes.Dishes/detailMore',['dishes_ids'=>(string)$dishId],$dishes,'more');
$request('NATION','/terminal/store.Nation/all',[],$sources['nation']);
$request('SPORT','/terminal/store.Sport/all',[],$sources['sport'],'tree');
}finally{\think\facade\Cache::delete($token);}
$after=$snapshot();$events=[];$dml=0;foreach(file($runtime.'/ai_queries.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line){$e=json_decode($line,true);$events[]=$e;if(in_array($e['operation']??'',['insert','update','delete','replace','merge'],true))++$dml;}
$files=[];$actual=dirname((new ReflectionClass(\app\terminal\controller\Enum::class))->getFileName(),4);foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($actual.'/app/terminal',FilesystemIterator::SKIP_DOTS)) as $f)if($f->getExtension()==='php'&&strpos($f->getFilename(),'._')!==0)$files[substr($f->getPathname(),strlen($actual)+1)]=hash_file('sha256',$f->getPathname());
$report=['lane'=>$lane,'cases'=>$cases,'case_count'=>count($cases),'source_failures'=>count(array_filter($cases,static function($c){return !$c['source_projection_equal']||$c['http_status']!==200||$c['business_status']!==200;})),'source_before_after_unchanged'=>$before===$after,'snapshot'=>$after,'non_select_observed'=>$dml,'boundary_verified'=>true,'token_cleaned'=>!\think\facade\Cache::has($token),'terminal_source_sha256'=>$files,'events'=>$events,'scope'=>'partial_read_paths_only'];
file_put_contents($runtime.'/report.json',json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));chmod($runtime.'/report.json',0600);file_put_contents($runtime.'/responses.json',json_encode($bodies,JSON_UNESCAPED_UNICODE));chmod($runtime.'/responses.json',0600);
echo json_encode(['lane'=>$lane,'cases'=>count($cases),'source_failures'=>$report['source_failures'],'unchanged'=>$before===$after,'non_select'=>$dml,'token_cleaned'=>$report['token_cleaned'],'runtime'=>$runtime]),"\n";
exit($report['source_failures']||$before!==$after||$dml?1:0);
