<?php
declare(strict_types=1);
date_default_timezone_set('Asia/Shanghai');
$lane=getenv('PILOT_LANE');$root=getenv('FILES_APP_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:1831[12]$#',$base))exit(78);
$tenant=1930301010;$marker='CDXFM10B27A4D6E9C01';$host='cdx-file-metadata.invalid';
define('TP_ENV','poc');define('BUSINESS','aizhct');define('ENTERPRISE_NO','10001');
$loader=require $root.'/vendor/autoload.php';$loader->setPsr4('app\\',$root.'/app');$loader->setPsr4('cores\\',$root.'/cores');require $root.'/external_stubs.php';
$storageAttempts=0;spl_autoload_register(static function($class)use(&$storageAttempts):void{if(strpos(strtolower($class),'app\\common\\library\\storage\\')===0||strtolower($class)==='app\\common\\library\\upload'){++$storageAttempts;throw new RuntimeException('Storage/upload forbidden');}},true,true);
$app=new \think\App($root);$app->setEnvName('poc');$app->setRuntimePath($runtime.'/runner/');$app->initialize();$app->debug(false);$app->env->set('APP_DEBUG',false);\think\App::$store_id=$tenant;
$app->config->set(['default'=>'file','stores'=>['file'=>['type'=>'File','path'=>$runtime.'/cache/','prefix'=>'','expire'=>0]]],'cache');$app->config->set(['close'=>true,'default'=>'file','channels'=>['file'=>['type'=>'FileFormat','path'=>$runtime.'/logs/','close'=>true]]],'log');
$newPdo=static function()use($dm):PDO{$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';return new PDO($dsn,getenv('POC_AI_USER'),getenv('POC_AI_PASSWORD'),[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_CASE=>PDO::CASE_LOWER]);};
$pdo=$newPdo();$table=static function($name)use($dm):string{return $dm?strtoupper('yoshop_'.$name):'yoshop_'.$name;};
$q=static function($sql,$args=[])use($pdo):array{$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;};
$canonical=static function($v)use(&$canonical){if(!is_array($v))return $v;if($v!==[]&&array_keys($v)!==range(0,count($v)-1))ksort($v);foreach($v as &$x)$x=$canonical($x);return $v;};
$same=static function($a,$b)use($canonical):bool{return $canonical($a)===$canonical($b);};
$snapshot=static function()use($pdo,$table):array{$o=[];foreach(['upload_file','upload_group'] as $t){$s=$pdo->query('SELECT * FROM '.$table($t));$h=[];while($r=$s->fetch(PDO::FETCH_ASSOC))$h[]=hash('sha256',json_encode($r,JSON_UNESCAPED_UNICODE));sort($h);$o[$t]=['rows'=>count($h),'sha256'=>hash('sha256',implode("\n",$h))];}return $o;};
$ownedGroups=static function()use($q,$table,$tenant):array{return $q('SELECT * FROM '.$table('upload_group').' WHERE STORE_ID=? ORDER BY SORT,CREATE_TIME',[$tenant]);};
$ownedFiles=static function()use($q,$table,$tenant):array{return $q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? ORDER BY FILE_ID DESC',[$tenant]);};
$rowContract=static function(array $r,string $kind):array{foreach($r as $k=>&$v){if(in_array($k,['create_time','update_time'],true))$v=(int)$v>0?date('Y-m-d H:i:s',(int)$v):'';elseif(in_array($k,['file_id','group_id','parent_id','channel','file_type','file_size','uploader_id','is_recycle','is_delete','store_id','sort'],true))$v=(int)$v;}unset($v);return $r;};
$groupTree=static function(array $rows,int $parent=0)use(&$groupTree,$rowContract):array{$out=[];foreach($rows as $r)if((int)$r['parent_id']===$parent){$item=$rowContract($r,'group');$child=$groupTree($rows,(int)$r['group_id']);if($child)$item['children']=$child;$out[]=$item;}return $out;};
$wireShape=static function($v)use(&$wireShape){if(is_object($v)){$p=[];foreach(get_object_vars($v) as $k=>$x)$p[$k]=$wireShape($x);ksort($p);return ['type'=>'object','properties'=>$p];}if(is_array($v))return ['type'=>'array','items'=>array_map($wireShape,$v)];return ['type'=>(is_int($v)||is_float($v))?'number':gettype($v)];};
$before=$snapshot();foreach(['upload_file','upload_group','store'] as $t)if((int)$q('SELECT COUNT(*) AS N FROM '.$table($t).' WHERE STORE_ID=?',[$tenant])[0]['n']!==0)throw new RuntimeException('Synthetic context collision');
foreach(['upload_file','upload_group'] as $t){$n=$table($t);$tr=$dm?$q("SELECT TRIGGER_NAME FROM USER_TRIGGERS WHERE TABLE_NAME='".$n."'"):$q("SHOW TRIGGERS WHERE `Table`='".$n."'");$fk=$dm?$q("SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME='".$n."' AND CONSTRAINT_TYPE='R'"):$q("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='".$n."' AND REFERENCED_TABLE_NAME IS NOT NULL");if($tr||$fk)throw new RuntimeException('Frozen metadata boundary changed');}
$token=bin2hex(random_bytes(24));\think\facade\Cache::set($token,['user'=>['store_id'=>$tenant,'store_user_id'=>1930301011,'real_name'=>$marker,'user_name'=>$marker,'is_super'=>1],'store_id'=>$tenant,'is_login'=>true],3600);
$boundary=json_decode(file_get_contents($base.'/__poc_boundary'),true);if(empty($boundary['metadata_allowlist'])||empty($boundary['external_stubs'])||empty($boundary['new_private_app_root']))throw new RuntimeException('Boundary absent');
foreach(['queries.jsonl','list_guards.jsonl','storage_guard.jsonl'] as $stream){file_put_contents($runtime.'/'.$stream,'');chmod($runtime.'/'.$stream,0600);}
$cases=[];$responses=[];$sourceScenarios=[];$map=['marker'=>$marker,'tenant'=>$tenant,'files'=>[],'groups'=>[]];$cleanup=[];$unexpected=null;
$request=static function(string $name,string $method,string $path,array $args=[],?array $json=null,int $expected=200)use($base,$host,$token,$wireShape,&$responses):array{$opts=['method'=>$method,'ignore_errors'=>true,'timeout'=>25,'header'=>"Host: ".$host."\r\nPlatform: POC\r\nAccess-Token: ".$token."\r\nX-Pilot-Scenario: PILOT_FILES_".$name."\r\n"];if($json!==null){$opts['header'].="Content-Type: application/json\r\n";$opts['content']=json_encode($json,JSON_UNESCAPED_UNICODE);}$raw=file_get_contents($base.$path.($args?'?'.http_build_query($args):''),false,stream_context_create(['http'=>$opts]));preg_match('#^HTTP/\S+\s+(\d+)#',$http_response_header[0]??'',$m);$body=json_decode((string)$raw,true);$responses[$name]=['http_status'=>(int)($m[1]??0),'body'=>json_decode((string)$raw),'wire_shape'=>$wireShape(json_decode((string)$raw))];return ['name'=>$name,'method'=>$method,'path'=>$path,'http_status'=>(int)($m[1]??0),'body'=>$body,'expected'=>$expected,'status_ok'=>(int)($m[1]??0)===200&&($body['status']??null)===$expected];};
$record=static function(array $r,bool $source=true,int $rows=0)use(&$cases):void{$cases[]=['scenario'=>$r['name'],'method'=>$r['method'],'path'=>$r['path'],'expected_business_status'=>$r['expected'],'source_equal'=>$source,'positive_rows'=>$rows,'pass'=>$r['status_ok']&&$source];};
$readGroups=static function(string $name,string $module)use($request,$record,$ownedGroups,$groupTree,$same,&$sourceScenarios):void{$rows=$ownedGroups();$r=$request($name,'GET','/'.$module.'/files.Group/list');$expected=$groupTree($rows);$record($r,$same($r['body']['data']['list']??null,$expected),count($rows));$sourceScenarios[$name]=['groups'=>$rows];};
$readFiles=static function(string $name,string $module,array $args=[])use($request,$record,$ownedFiles,$rowContract,$same,$host,&$sourceScenarios):void{$rows=$ownedFiles();$selected=[];foreach($rows as $r){if((int)$r['channel']!==10||(int)$r['is_delete']!==0||(int)$r['is_recycle']!==0)continue;if(isset($args['groupId'])&&(int)$r['group_id']!==(int)$args['groupId'])continue;if(isset($args['fileType'])&&(int)$r['file_type']!==(int)$args['fileType'])continue;$item=$rowContract($r,'file');$item['external_url']='http://'.$host.'/uploads/'.$r['file_path'];$item['preview_url']=(int)$r['file_type']===10?$item['external_url']:'http://'.$host.'/assets/store/img/file_preview/video.png';$selected[]=$item;}$expected=['total'=>count($selected),'per_page'=>15,'current_page'=>1,'last_page'=>(int)ceil(count($selected)/15),'data'=>array_slice($selected,0,15)];$r=$request($name,'GET','/'.$module.'/Files/list',$args);$record($r,$same($r['body']['data']['list']??null,$expected),count($selected));$sourceScenarios[$name]=['files'=>$rows,'source_rows_only'=>true];};
try{
    foreach(['store','terminal'] as $mi=>$module){$tag=$module==='store'?'S':'T';$parentName=$marker.'_'.$tag.'父';$childName=$marker.'_'.$tag.'子';$sort=10+$mi*10;
        $r=$request($tag.'_GROUP_ADD_PARENT','POST','/'.$module.'/files.Group/add',[],['form'=>['name'=>$parentName,'parent_id'=>0,'sort'=>$sort]]);$rows=$q('SELECT * FROM '.$table('upload_group').' WHERE STORE_ID=? AND NAME=?',[$tenant,$parentName]);$record($r,count($rows)===1&&(int)$rows[0]['parent_id']===0&&(int)$rows[0]['sort']===$sort,1);if(count($rows)!==1)throw new RuntimeException('Parent group unavailable');$parent=(int)$rows[0]['group_id'];$map['groups'][$tag.'_P']=$parent;
        $r=$request($tag.'_GROUP_ADD_CHILD','POST','/'.$module.'/files.Group/add',[],['form'=>['name'=>$childName,'parent_id'=>$parent,'sort'=>$sort+1]]);$rows=$q('SELECT * FROM '.$table('upload_group').' WHERE STORE_ID=? AND NAME=?',[$tenant,$childName]);$record($r,count($rows)===1&&(int)$rows[0]['parent_id']===$parent&&(int)$rows[0]['sort']===$sort+1,1);if(count($rows)!==1)throw new RuntimeException('Child group unavailable');$child=(int)$rows[0]['group_id'];$map['groups'][$tag.'_C']=$child;
        foreach([$parent,$child] as $gid){if((int)$q('SELECT COUNT(*) AS N FROM '.$table('upload_file').' WHERE GROUP_ID=? AND STORE_ID<>?',[$gid,$tenant])[0]['n']!==0||(int)$q('SELECT COUNT(*) AS N FROM '.$table('upload_group').' WHERE PARENT_ID=? AND STORE_ID<>?',[$gid,$tenant])[0]['n']!==0)throw new RuntimeException('Natural record references generated fixture group');}
        $readGroups($tag.'_GROUP_LIST',$module);
        $ownedBefore=$ownedGroups();$r=$request($tag.'_GROUP_CYCLE_REJECT','POST','/'.$module.'/files.Group/edit',[],['groupId'=>$parent,'form'=>['name'=>$parentName,'parent_id'=>$child,'sort'=>$sort]],500);$record($r,$same($ownedBefore,$ownedGroups()));
        $edited=$marker.'_'.$tag."'新子'";$r=$request($tag.'_GROUP_EDIT','POST','/'.$module.'/files.Group/edit',[],['groupId'=>$child,'form'=>['name'=>$edited,'parent_id'=>$parent,'sort'=>$sort+2]]);$row=$q('SELECT * FROM '.$table('upload_group').' WHERE STORE_ID=? AND GROUP_ID=?',[$tenant,$child]);$record($r,count($row)===1&&$row[0]['name']===$edited&&(int)$row[0]['parent_id']===$parent&&(int)$row[0]['sort']===$sort+2,1);$readGroups($tag.'_GROUP_LIST_EDITED',$module);
        foreach([10,20] as $fi=>$type){$logical=$tag.'_'.($type===10?'IMAGE':'ANNEX');$fileName=$marker.'_'.$logical.'.jpg';$filePath=$marker.'/'.$logical.'-metadata.jpg';if(file_exists($root.'/public/uploads/'.$filePath))throw new RuntimeException('Actual upload path exists');$class='app\\'.$module.'\\model\\UploadFile';$model=new $class;if(!$model->add(['channel'=>10,'storage'=>'local','domain'=>'https://example.invalid/'.$marker,'file_name'=>$fileName,'file_path'=>$filePath,'file_size'=>17+$fi,'file_ext'=>'jpg'], $type,$parent,$tenant))throw new RuntimeException('Actual Model metadata prepare failed');$row=$q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? AND FILE_PATH=?',[$tenant,$filePath]);if(count($row)!==1||(string)$model->getData('file_id')!==(string)$row[0]['file_id'])throw new RuntimeException('Metadata original ID ownership failed');foreach(['channel'=>10,'storage'=>'local','domain'=>'https://example.invalid/'.$marker,'file_name'=>$fileName,'file_path'=>$filePath,'file_size'=>17+$fi,'file_ext'=>'jpg','file_type'=>$type,'group_id'=>$parent,'store_id'=>$tenant] as $field=>$expectedValue){if(is_int($expectedValue)?(string)$row[0][$field]!== (string)$expectedValue:$row[0][$field]!==$expectedValue)throw new RuntimeException('Prepared metadata field mismatch');}$map['files'][$logical]=(int)$row[0]['file_id'];}
        $readFiles($tag.'_FILES_LIST',$module);
        $image=$map['files'][$tag.'_IMAGE'];$annex=$map['files'][$tag.'_ANNEX'];$rename=$marker.'_'.$tag.'中文 "重命名".jpg';$r=$request($tag.'_FILES_EDIT','POST','/'.$module.'/Files/edit',[],['fileId'=>$image,'form'=>['file_name'=>$rename,'group_id'=>$child]]);$row=$q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? AND FILE_ID=?',[$tenant,$image]);$record($r,count($row)===1&&$row[0]['file_name']===htmlspecialchars(trim($rename),ENT_COMPAT)&&(int)$row[0]['group_id']===$child,1);$readFiles($tag.'_FILES_LIST_RENAMED',$module);
        $r=$request($tag.'_MOVE_SINGLE','POST','/'.$module.'/Files/moveGroup',[],['groupId'=>$parent,'fileIds'=>[$image]]);$row=$q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? AND FILE_ID=?',[$tenant,$image]);$record($r,count($row)===1&&(int)$row[0]['group_id']===$parent,1);$readFiles($tag.'_FILES_SINGLE_PARENT',$module,['groupId'=>$parent]);
        $r=$request($tag.'_MOVE_MULTIPLE','POST','/'.$module.'/Files/moveGroup',[],['groupId'=>$child,'fileIds'=>[$image,$annex,$image]]);$rows=$q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? AND FILE_ID IN (?,?)',[$tenant,$image,$annex]);$ok=count($rows)===2;foreach($rows as $row)$ok=$ok&&(int)$row['group_id']===$child;$record($r,$ok,2);$readFiles($tag.'_FILES_CHILD_MULTIPLE',$module,['groupId'=>$child]);
        $r=$request($tag.'_MOVE_UNGROUPED','POST','/'.$module.'/Files/moveGroup',[],['groupId'=>0,'fileIds'=>[$image,$annex]]);$rows=$q('SELECT * FROM '.$table('upload_file').' WHERE STORE_ID=? AND FILE_ID IN (?,?)',[$tenant,$image,$annex]);$ok=count($rows)===2;foreach($rows as $row)$ok=$ok&&(int)$row['group_id']===0;$record($r,$ok,2);$readFiles($tag.'_FILES_UNGROUPED',$module,['groupId'=>0]);$readFiles($tag.'_FILES_IMAGE_FILTER',$module,['fileType'=>10]);
    }
}catch(Throwable $e){$unexpected=['class'=>get_class($e),'file'=>basename($e->getFile()),'line'=>$e->getLine(),'message_sha256'=>hash('sha256',$e->getMessage())];}finally{
    foreach($ownedFiles() as $row){if(strpos($row['file_path'],$marker)!==0)throw new RuntimeException('Unknown metadata ownership, cleanup refused');$s=$pdo->prepare('DELETE FROM '.$table('upload_file').' WHERE FILE_ID=? AND STORE_ID=? AND FILE_PATH=?');$s->execute([$row['file_id'],$tenant,$row['file_path']]);$cleanup['files']=($cleanup['files']??0)+$s->rowCount();}
    $groups=$ownedGroups();foreach(array_reverse($groups) as $row){if(strpos($row['name'],$marker)!==0)throw new RuntimeException('Unknown group ownership, cleanup refused');$s=$pdo->prepare('DELETE FROM '.$table('upload_group').' WHERE GROUP_ID=? AND STORE_ID=? AND NAME=?');$s->execute([$row['group_id'],$tenant,$row['name']]);$cleanup['groups']=($cleanup['groups']??0)+$s->rowCount();}
}
if($unexpected===null)foreach(['store','terminal'] as $module){$tag=$module==='store'?'S':'T';$readFiles($tag.'_FILES_CLEANED',$module);$readGroups($tag.'_GROUPS_CLEANED',$module);}
\think\facade\Cache::delete($token);\think\facade\Cache::clear();$audit=$newPdo();$remaining=[];foreach(['upload_file','upload_group','store'] as $t){$s=$audit->prepare('SELECT COUNT(*) FROM '.$table($t).' WHERE STORE_ID=?');$s->execute([$tenant]);$remaining[$t]=(int)$s->fetchColumn();}
$after=$snapshot();$events=[];foreach(file($runtime.'/queries.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line)$events[]=json_decode($line,true);$guards=[];foreach(file($runtime.'/list_guards.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line)$guards[]=json_decode($line,true);$guardHits=[];foreach(file($runtime.'/storage_guard.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line)$guardHits[]=json_decode($line,true);
$sha=[];foreach(['app/store/controller/Files.php','app/terminal/controller/Files.php','app/store/controller/files/Group.php','app/terminal/controller/files/Group.php','app/store/model/UploadFile.php','app/terminal/model/UploadFile.php','app/common/model/UploadFile.php','app/store/model/UploadGroup.php','app/terminal/model/UploadGroup.php','app/common/model/UploadGroup.php','app/common/database/DmBuilder.php','cores/library/Version.php'] as $f)$sha[$f]=hash_file('sha256',$root.'/'.$f);
$report=['lane'=>$lane,'cases'=>$cases,'case_count'=>count($cases),'unexpected_error'=>$unexpected,'failures'=>count(array_filter($cases,static function($c){return !$c['pass'];})),'before'=>$before,'after'=>$after,'natural_unchanged'=>$before===$after,'cleanup'=>$cleanup,'independent_remaining'=>$remaining,'token_cleaned'=>!\think\facade\Cache::has($token),'list_environment_guards'=>$guards,'storage_guard_attempts'=>$storageAttempts+array_sum(array_column($guardHits,'forbidden_attempts')),'source_sha256'=>$sha,'events'=>$events,'host_header'=>$host,'scope'=>'12 explicit metadata methods, no delete/upload/storage/file removal branches'];
foreach(['report.json'=>$report,'responses.json'=>$responses,'fixture_map.json'=>$map,'source-scenarios.json'=>$sourceScenarios] as $f=>$d){file_put_contents($runtime.'/'.$f,json_encode($d,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));chmod($runtime.'/'.$f,0600);}
echo json_encode(['lane'=>$lane,'cases'=>count($cases),'failures'=>$report['failures'],'unchanged'=>$before===$after,'remaining'=>$remaining,'storage_attempts'=>$report['storage_guard_attempts'],'runtime'=>$runtime]),"\n";exit($unexpected!==null||$report['failures']||$before!==$after||array_sum($remaining)||$report['storage_guard_attempts']?1:0);
