<?php
declare(strict_types=1);
date_default_timezone_set('Asia/Shanghai');
$lane=(string)getenv('CATEGORY_LANE');$base=(string)getenv('CATEGORY_BASE');$runtime=(string)getenv('CATEGORY_RUNTIME');
if(!in_array($lane,['B','C'],true)||!preg_match('#^http://127\.0\.0\.1:1828[12]$#D',$base))exit(78);
$dm=getenv('POC_DIALECT')==='dm';
$connect=static function()use($dm):PDO{return new PDO($dm?'dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP':
    'mysql:host='.getenv('POC_AI_HOST').';port='.getenv('POC_AI_PORT').';dbname='.getenv('POC_AI_SCHEMA').';charset=utf8',
    getenv('POC_AI_USER'),getenv('POC_AI_PASSWORD'),[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_CASE=>PDO::CASE_LOWER]);};
$db=$connect();
$assert=static function(bool $ok,string $reason):void{if(!$ok)throw new RuntimeException($reason);};
if($dm)$assert($db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn()==='AIZHCT_APP','DM identity');
else $assert(strpos((string)$db->query('SELECT VERSION()')->fetchColumn(),'5.6.')===0
    &&$db->query('SELECT DATABASE()')->fetchColumn()===getenv('POC_AI_SCHEMA'),'local MySQL identity');
$query=static function(string $sql,array $args=[])use($db):array{
    $s=$db->prepare($sql);$s->execute($args);$rows=$s->fetchAll(PDO::FETCH_ASSOC);
    foreach($rows as &$row)foreach($row as &$value)if(is_resource($value))$value=stream_get_contents($value);
    unset($row,$value);return $rows;
};
$prefix='CDXAC10F7A2';$tenants=['store'=>2147000081,'terminal'=>2147000082];
$ownedCounts=static function()use($query):array{return [
    'categories'=>(int)$query('SELECT COUNT(*) n FROM yoshop_article_category WHERE store_id IN (2147000081,2147000082)')[0]['n'],
    'articles'=>(int)$query('SELECT COUNT(*) n FROM yoshop_article WHERE store_id IN (2147000081,2147000082)')[0]['n']];};
if(($argv[1]??'')==='audit'){$counts=$ownedCounts();echo json_encode(['lane'=>$lane,'independent_tenant_rows'=>$counts]),"\n";exit(array_sum($counts)?1:0);}
$assert(array_sum($ownedCounts())===0,'Private tenant collision');
$assert(!$query('SELECT category_id FROM yoshop_article_category WHERE name LIKE ?',[$prefix.'%'])
    &&!$query('SELECT article_id FROM yoshop_article WHERE title LIKE ?',[$prefix.'%']),'Prefix collision');
$columns=$dm?$query("SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME='YOSHOP_ARTICLE_CATEGORY'")
    :$query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_article_category'");
$columnNames=array_map(static function(array $row):string{return strtolower($row['column_name']);},$columns);sort($columnNames);
$expectedColumns=['category_id','name','status','sort','store_id','create_time','update_time'];sort($expectedColumns);
$assert($columnNames===$expectedColumns,'Formal category columns drift');
$triggers=$dm?$query("SELECT COUNT(*) n FROM USER_TRIGGERS WHERE TABLE_NAME IN ('YOSHOP_ARTICLE_CATEGORY','YOSHOP_ARTICLE')")
    :$query("SELECT COUNT(*) n FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA=DATABASE() AND EVENT_OBJECT_TABLE IN ('yoshop_article_category','yoshop_article')");
$fks=$dm?$query("SELECT COUNT(*) n FROM USER_CONSTRAINTS WHERE CONSTRAINT_TYPE='R' AND (TABLE_NAME IN ('YOSHOP_ARTICLE_CATEGORY','YOSHOP_ARTICLE') OR R_CONSTRAINT_NAME IN (SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME IN ('YOSHOP_ARTICLE_CATEGORY','YOSHOP_ARTICLE')))")
    :$query("SELECT COUNT(*) n FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL AND (TABLE_NAME IN ('yoshop_article_category','yoshop_article') OR REFERENCED_TABLE_NAME IN ('yoshop_article_category','yoshop_article'))");
$assert((int)$triggers[0]['n']===0&&(int)$fks[0]['n']===0,'Trigger/FK side effect gate');
$snapshot=static function()use($query):array{$out=[];foreach(['article_category'=>'category_id','article'=>'article_id'] as $table=>$pk){
    $rows=$query('SELECT * FROM yoshop_'.$table.' ORDER BY '.$pk);$out[$table]=['rows'=>count($rows),'sha'=>hash('sha256',serialize($rows))];}return $out;};
$before=$snapshot();
$boundary=json_decode((string)file_get_contents($base.'/__poc_boundary'),true);
$assert(!empty($boundary['external_stubs'])&&!empty($boundary['private_cache'])&&$boundary['allowlist']==='article-category-only','Private boundary');
$root=(string)getenv('CATEGORY_ROOT');define('TP_ENV','poc');define('BUSINESS','aizhct');define('ENTERPRISE_NO','10001');require $root.'/vendor/autoload.php';
$app=new \think\App($root);$app->setEnvName('poc');$app->setRuntimePath($runtime.'/fixture-runtime/');$app->initialize();
$cache=new \think\cache\driver\File($app,['path'=>$runtime.'/cache/','prefix'=>'','expire'=>0]);
$tokens=[];foreach($tenants as $module=>$tenant){$tokens[$module]=bin2hex(random_bytes(24));
    $cache->set($tokens[$module],['user'=>['store_user_id'=>$tenant,'store_id'=>$tenant,'real_name'=>$prefix,'user_name'=>$prefix,'is_super'=>1],
        'store_id'=>$tenant,'is_login'=>true],1800);}
$cases=[];$ids=[];$articleIds=[];$error=null;$privateResponses=[];
file_put_contents($runtime.'/queries.jsonl','');chmod($runtime.'/queries.jsonl',0600);
$request=static function(string $module,string $scenario,string $action,array $args=[],array $form=[],string $encoding='json')use($base,$tokens,&$privateResponses):array{
    $method=$action==='list'?'GET':'POST';$scenario='PILOT_AC_'.strtoupper($module).'_'.$scenario;
    $url=$base.'/'.$module.'/content.article.Category/'.$action.($args?'?'.http_build_query($args):'');
    $header='Host: '.$module.".ac-test.invalid\r\nAccess-Token: ".$tokens[$module]."\r\nPlatform: POC\r\nX-Pilot-Scenario: ".$scenario."\r\nContent-Type: ".($encoding==='json'?'application/json':'application/x-www-form-urlencoded')."\r\n";
    $content=$encoding==='json'?json_encode(['form'=>$form],JSON_UNESCAPED_UNICODE):http_build_query(['form'=>$form]);
    $raw=file_get_contents($url,false,stream_context_create(['http'=>['method'=>$method,'header'=>$header,'content'=>$content,'timeout'=>30,'ignore_errors'=>true]]));
    preg_match('#^HTTP/\S+\s+(\d+)#',$http_response_header[0]??'',$match);$body=json_decode((string)$raw,true);
    if(!is_array($body))throw new RuntimeException('Non JSON '.$scenario);
    $privateResponses[$scenario]=['http_status'=>(int)($match[1]??0),'body'=>$body];
    return ['scenario'=>$scenario,'method'=>$method,'path'=>'/'.$module.'/content.article.Category/'.$action,
        'encoding'=>$encoding,'http_status'=>(int)($match[1]??0),'body'=>$body];
};
$expect=static function(array $case,int $status=200)use($assert):void{$assert($case['http_status']===200&&($case['body']['status']??null)===$status,'Business failure '.$case['scenario']);};
$read=static function(string $module,string $scenario,int $expectedRows)use($request,$expect,$assert,$query,$tenants,&$cases):void{
    $case=$request($module,$scenario,'list');$expect($case);
    $rows=$query('SELECT * FROM yoshop_article_category WHERE store_id=? ORDER BY sort,category_id',[$tenants[$module]]);
    $list=$case['body']['data']['list']??null;
    $assert(is_array($list)&&count($list)===$expectedRows&&count($rows)===$expectedRows,'Read full list cardinality');
    foreach($list as $index=>&$row){$source=$rows[$index];
        $actualKeys=array_keys($row);sort($actualKeys);$expectedKeys=array_keys($source);sort($expectedKeys);
        $assert($actualKeys===$expectedKeys,'Full response/source keys');
        foreach($source as $key=>$value){
            if(in_array($key,['create_time','update_time'],true)){
                $assert($row[$key]===date('Y-m-d H:i:s',(int)$value),'Timestamp/source mismatch');
                if($key==='update_time')$row[$key]='INDEPENDENTLY_VERIFIED_UPDATE_TIME';
            }elseif(is_numeric($value))$assert(is_int($row[$key])&&$row[$key]===(int)$value,'Integer/source mismatch');
            else $assert($row[$key]===$value,'Text/source mismatch');
        }
    }unset($row);
    $case['body']['data']['list']=$list;$case['independent_source_rows']=$expectedRows;$case['all_source_fields_equal']=true;$cases[]=$case;
};
try{
foreach($tenants as $module=>$tenant){
    $read($module,'LIST_EMPTY_BEFORE',0);
    $name=$prefix.'_'.$module.'_中文\'分类\\';
    $form=['name'=>$name,'status'=>0,'sort'=>2147000081,'create_time'=>1609459200,'update_time'=>1609459200];
    $case=$request($module,'ADD_JSON','add',[],$form);$expect($case);
    $rows=$query('SELECT * FROM yoshop_article_category WHERE name=? AND store_id=?',[$name,$tenant]);
    $assert(count($rows)===1&&(int)$rows[0]['status']===0&&(int)$rows[0]['sort']===2147000081,'Add original row');
    $first=(int)$rows[0]['category_id'];$ids[$first]=1;
    $case['independent_state']=['rows'=>1,'name_equal'=>true,'status'=>0,'sort'=>2147000081,'private_tenant_equal'=>true];$cases[]=$case;
    $form2=$form;$form2['name']=$prefix.'_'.$module.'_第二';$form2['status']=1;$form2['sort']=2147000082;
    $case=$request($module,'ADD_FORM','add',[],$form2,'form');$expect($case);
    $rows=$query('SELECT * FROM yoshop_article_category WHERE name=? AND store_id=?',[$form2['name'],$tenant]);
    $assert(count($rows)===1&&(int)$rows[0]['status']===1&&(int)$rows[0]['sort']===2147000082,'Second add original row');
    $second=(int)$rows[0]['category_id'];$ids[$second]=2;$case['independent_state']=['rows'=>1,'name_equal'=>true,'status'=>1,'sort'=>2147000082];$cases[]=$case;
    $read($module,'LIST_TWO',2);
    $edit=$form;$edit['name']=$prefix.'_'.$module.'_修改\'中文\\';$edit['status']=1;$edit['sort']=2147000083;
    $case=$request($module,'EDIT_FORM','edit',['categoryId'=>$first],$edit,'form');$expect($case);
    $rows=$query('SELECT * FROM yoshop_article_category WHERE category_id=? AND store_id=?',[$first,$tenant]);
    $assert(count($rows)===1&&$rows[0]['name']===$edit['name']&&(int)$rows[0]['status']===1&&(int)$rows[0]['sort']===2147000083,'Edit original row');
    $case['independent_state']=['rows'=>1,'name_equal'=>true,'status'=>1,'sort'=>2147000083];$cases[]=$case;
    $read($module,'LIST_SORTED',2);
    // Approved owned article fixture only: no article HTTP route/upload/OSS involved.
    $articleTitle=$prefix.'_'.$module.'_REFERENCE';
    $insert=$db->prepare('INSERT INTO yoshop_article (title,show_type,category_id,image_id,content,sort,status,virtual_views,actual_views,is_delete,store_id,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)');
    $insert->execute([$articleTitle,10,$first,0,$prefix.'_CONTENT',0,0,0,0,0,$tenant,1609459200,1609459200]);
    $article=$query('SELECT * FROM yoshop_article WHERE title=? AND store_id=?',[$articleTitle,$tenant]);
    $assert(count($article)===1&&(int)$article[0]['category_id']===$first&&(int)$article[0]['is_delete']===0,'Own article fixture state');
    $articleId=(int)$article[0]['article_id'];$articleIds[$articleId]=$tenant;
    $case=$request($module,'DELETE_REFERENCED_REJECT','delete',['categoryId'=>$first]);$expect($case,500);
    $assert($case['body']['message']==='该分类下存在1个文章，不允许删除'
        &&count($query('SELECT category_id FROM yoshop_article_category WHERE category_id=? AND store_id=?',[$first,$tenant]))===1,'Referenced refusal original row');
    $case['independent_state']=['category_rows'=>1,'active_owned_articles'=>1,'delete_rejected'=>true];$cases[]=$case;
    $read($module,'LIST_AFTER_REJECT',2);
    $soft=$db->prepare('UPDATE yoshop_article SET is_delete=1 WHERE article_id=? AND title=? AND store_id=?');$soft->execute([$articleId,$articleTitle,$tenant]);
    $article=$query('SELECT is_delete FROM yoshop_article WHERE article_id=?',[$articleId]);
    $assert(count($article)===1&&(int)$article[0]['is_delete']===1,'Fixture article soft state');
    $case=$request($module,'DELETE_AFTER_ARTICLE_SOFT_STATE','delete',['categoryId'=>$first]);$expect($case);
    $assert(!$query('SELECT category_id FROM yoshop_article_category WHERE category_id=? AND store_id=?',[$first,$tenant]),'Category physical deletion');
    $case['independent_state']=['category_physical_rows'=>0,'article_physical_rows'=>1,'article_is_delete'=>1,'soft_article_excluded_from_guard'=>true];$cases[]=$case;
    $read($module,'LIST_ONE',1);
    $case=$request($module,'DELETE_SECOND','delete',['categoryId'=>$second]);$expect($case);
    $assert(!$query('SELECT category_id FROM yoshop_article_category WHERE category_id=? AND store_id=?',[$second,$tenant]),'Second physical deletion');
    $case['independent_state']=['category_physical_rows'=>0];$cases[]=$case;
    $read($module,'LIST_EMPTY_AFTER',0);
}
}catch(Throwable $caught){$error=['class'=>get_class($caught),'message'=>$caught->getMessage(),'line'=>$caught->getLine()];}
finally{
    $articles=$query('SELECT article_id,title,store_id FROM yoshop_article WHERE store_id IN (2147000081,2147000082) AND title LIKE ?',[$prefix.'%']);
    foreach($articles as $row){$delete=$db->prepare('DELETE FROM yoshop_article WHERE article_id=? AND title=? AND store_id=?');$delete->execute([$row['article_id'],$row['title'],$row['store_id']]);}
    $categories=$query('SELECT category_id,name,store_id FROM yoshop_article_category WHERE store_id IN (2147000081,2147000082) AND name LIKE ?',[$prefix.'%']);
    foreach($categories as $row){$delete=$db->prepare('DELETE FROM yoshop_article_category WHERE category_id=? AND name=? AND store_id=?');$delete->execute([$row['category_id'],$row['name'],$row['store_id']]);}
    foreach($tokens as $token)$cache->delete($token);
}
$fresh=$connect();$remaining=[];foreach(['article_category','article'] as $table){$s=$fresh->query('SELECT COUNT(*) FROM yoshop_'.$table.' WHERE store_id IN (2147000081,2147000082)');$remaining[$table]=(int)$s->fetchColumn();}
$tokenAbsent=true;foreach($tokens as $token)$tokenAbsent=$tokenAbsent&&$cache->get($token)===null;
$cleanup=['fresh_pdo_categories'=>$remaining['article_category'],'fresh_pdo_articles'=>$remaining['article'],
    'tokens_absent'=>$tokenAbsent,'natural_tables_unchanged'=>$snapshot()===$before];
$normalise=static function($value)use(&$normalise,$ids){if(!is_array($value))return $value;foreach($value as $key=>&$part){
    if($key==='category_id'&&is_int($part)&&isset($ids[$part]))$part=$ids[$part];else $part=$normalise($part);
}unset($part);return $value;};
foreach($cases as &$case){$trace=$case['body']['trace_id']??null;$case['trace_format_valid']=is_string($trace)&&preg_match('/^[0-9a-f]{13}$/D',$trace)===1;
    if($case['trace_format_valid'])$case['body']['trace_id']='VALID_DYNAMIC_TRACE';$case=$normalise($case);}unset($case);
$events=[];foreach(file($runtime.'/queries.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line)$events[]=json_decode($line,true);
$report=['lane'=>$lane,'boundary_verified'=>true,'metadata'=>['formal_category_columns_equal'=>true,'triggers'=>0,'foreign_keys'=>0],
    'private_tenant_host_mapping'=>true,'case_count'=>count($cases),'cases'=>$cases,'cleanup'=>$cleanup,'source_before'=>$before,'error'=>$error,'events'=>$events,
    'normalization'=>'validated trace; independently read category_id integers; own update_time checked field-by-field against original DB',
    'limitations'=>'Article fixture inserted/soft-marked via isolated PDO, not article endpoint acceptance; no unknown-ID/retry/invalid-length/permission/outage/performance claims'];
file_put_contents($runtime.'/report.json',json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));chmod($runtime.'/report.json',0600);
file_put_contents($runtime.'/private-responses.json',json_encode($privateResponses,JSON_UNESCAPED_UNICODE));chmod($runtime.'/private-responses.json',0600);
echo json_encode(['lane'=>$lane,'cases'=>count($cases),'cleanup'=>$cleanup,'error'=>$error,'report'=>$runtime.'/report.json']),"\n";
exit($error!==null||array_sum($remaining)!==0||!$tokenAbsent||!$cleanup['natural_tables_unchanged']?1:0);
