<?php
declare(strict_types=1);
// Reuse the two exact retained rows; never INSERT or physical DELETE.
date_default_timezone_set('Asia/Shanghai');ini_set('display_errors','0');ini_set('log_errors','0');require __DIR__.'/common.php';
$lane=getenv('PILOT_LANE');$root=getenv('COUPON_APP_ROOT');$runtime=getenv('PILOT_RUNTIME');$base=getenv('POC_BASE_URL');$tenant=1930501010;$marker='CDXAC10E4C6A28B1D09';$db=cpPdo();$dm=$lane==='C';
if(!in_array($base,['http://127.0.0.1:18361','http://127.0.0.1:18362'],true)){exit(78);}
$old=json_decode(file_get_contents($runtime.'/report.json'),true);$oldMap=json_decode(file_get_contents($runtime.'/fixture_map.json'),true);$ids=$oldMap['coupon_ids'];if(count($ids)!==2||$old['unexpected']!==null){throw new RuntimeException('Reviewed retained red batch required');}
$snapshot=static function()use($db,$tenant):array{return ['coupon'=>cpDigest(cpRows($db,'SELECT * FROM yoshop_coupon WHERE store_id<>?',[$tenant])),'user_coupon'=>cpDigest(cpRows($db,'SELECT * FROM yoshop_user_coupon'))];};$before=$snapshot();
$read=static function(string $module)use($db,$tenant,$marker,$dm):array{$cast=$dm?'VARCHAR(64)':'CHAR';return cpRows($db,'SELECT c.*,CAST(reduce_price AS '.$cast.') AS exact_reduce_price,CAST(min_price AS '.$cast.') AS exact_min_price,CAST(write_off_limit_amount AS '.$cast.') AS exact_write_off_limit_amount FROM yoshop_coupon c WHERE store_id=? AND name LIKE ? ORDER BY coupon_id',[$tenant,$marker.'_'.$module.'%']);};
foreach($ids as $module=>$id){$rows=$read($module);if(count($rows)!==1||(int)$rows[0]['coupon_id']!==$id||$rows[0]['is_delete']!==1){throw new RuntimeException('Retained ownership or soft-delete drift');}}
$app=cpBootstrap($root,$runtime);$token=bin2hex(random_bytes(24));\think\facade\Cache::set($token,['user'=>['store_id'=>$tenant,'store_user_id'=>1930501011,'real_name'=>$marker,'user_name'=>$marker,'is_super'=>1],'store_id'=>$tenant,'is_login'=>true],3600);file_put_contents($runtime.'/fixture_map.json',json_encode(['marker'=>$marker,'tenant'=>$tenant,'coupon_ids'=>$ids,'token'=>$token]));
$cases=[];$responses=[];$source=[];$fixtureWrites=[];$error=null;
$same=static function($a,$b)use(&$same):bool{if(gettype($a)!==gettype($b)){return false;}if(is_object($a)){$a=get_object_vars($a);$b=get_object_vars($b);ksort($a);ksort($b);}if(is_array($a)){if(array_keys($a)!==array_keys($b)){return false;}foreach($a as $k=>$v){if(!$same($v,$b[$k])){return false;}}return true;}return $a===$b;};
$expected=static function(array $row):object{$out=$row;foreach(['reduce_price','min_price','write_off_limit_amount'] as $f){$out[$f]=bcadd($row['exact_'.$f],'0',2);unset($out['exact_'.$f]);}$out['discount']=$row['discount']/10;$out['start_time']=$row['start_time']?date('Y/m/d',(int)$row['start_time']):null;$out['end_time']=$row['end_time']?date('Y/m/d',(int)$row['end_time']):null;$out['apply_range_config']=$row['apply_range_config']?json_decode($row['apply_range_config'],true):[];foreach(['create_time','update_time'] as $f){$out[$f]=$row[$f]?date('Y-m-d H:i:s',(int)$row[$f]):null;}$out['state']=['text'=>'正常','value'=>1];return json_decode(json_encode($out,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR));};
$request=static function(string $name,string $method,string $module,string $action,array $args=[])use($base,$tenant,$token,&$responses):array{$path='/'.$module.'/market.Coupon/'.$action;$opts=['method'=>$method,'header'=>"Host: cdx-admin-coupon.invalid\r\nPlatform: POC\r\nAccess-Token: ".$token."\r\nX-Pilot-Scenario: PILOT_ACP_".$name."\r\n",'ignore_errors'=>true,'follow_location'=>0,'timeout'=>25];$url=$base.$path;if($method==='GET'){$url.='?'.http_build_query(['storeId'=>$tenant]+$args);}else{$opts['header'].="Content-Type: application/json\r\n";$opts['content']=json_encode(['storeId'=>$tenant]+$args);} $start=time();$raw=file_get_contents($url,false,stream_context_create(['http'=>$opts]));$end=time();preg_match('~^HTTP/\S+ (\d+)~',$http_response_header[0]??'',$m);$r=['method'=>$method,'path'=>$path,'http_status'=>(int)($m[1]??0),'body'=>json_decode((string)$raw),'time_window'=>[$start,$end]];$responses[$name]=$r;return $r;};
$record=static function(string $name,array $r,bool $pass)use(&$cases):void{$cases[]=['case'=>$name,'method'=>$r['method'],'path'=>$r['path'],'http_status'=>$r['http_status'],'status'=>$r['body']->status??null,'source_pass'=>$pass];};
try{foreach($ids as $module=>$id){$tag=$module==='store'?'S':'T';
    $r=$request($tag.'_RETAINED_DETAIL','GET',$module,'detail',['couponId'=>$id]);$rows=$read($module);$record($tag.'_RETAINED_DETAIL',$r,$r['http_status']===200&&($r['body']->status??null)===200&&$same($expected($rows[0]),$r['body']->data->detail??null));$source[$tag.'_RETAINED_DETAIL']=['rows'=>$rows,'expected'=>$expected($rows[0])];
    $s=$db->prepare('UPDATE yoshop_coupon SET is_delete=0 WHERE coupon_id=? AND store_id=? AND name=? AND is_delete=1');$s->execute([$id,$tenant,$rows[0]['name']]);if($s->rowCount()!==1){throw new RuntimeException('Exact owned visibility restore failed');}$fixtureWrites[]=['module'=>$module,'rows'=>1,'operation'=>'owned is_delete 1 to 0'];
    foreach(['LIST'=>'list','DETAIL'=>'detail'] as $name=>$action){$r=$request($tag.'_'.$name,'GET',$module,$action,$action==='detail'?['couponId'=>$id]:[]);$rows=$read($module);$actual=$action==='detail'?($r['body']->data->detail??null):($r['body']->data->list->data[0]??null);$ok=$r['http_status']===200&&($r['body']->status??null)===200&&$same($expected($rows[0]),$actual);if($action==='list'){$ok=$ok&&($r['body']->data->list->total??null)===1&&count($r['body']->data->list->data)===1;}$record($tag.'_'.$name,$r,$ok);$source[$tag.'_'.$name]=['rows'=>$rows,'expected'=>$expected($rows[0])];}
    $r=$request($tag.'_DELETE','POST',$module,'delete',['couponId'=>$id]);$rows=$read($module);$record($tag.'_DELETE',$r,$r['http_status']===200&&($r['body']->status??null)===200&&$rows[0]['is_delete']===1&&(int)$rows[0]['update_time']>=$r['time_window'][0]&&(int)$rows[0]['update_time']<=$r['time_window'][1]);
    $r=$request($tag.'_EMPTY','GET',$module,'list');$record($tag.'_EMPTY',$r,$r['http_status']===200&&($r['body']->status??null)===200&&($r['body']->data->list->data??null)===[]&&($r['body']->data->list->total??null)===0);
}}catch(Throwable $e){$error=['class'=>get_class($e),'file'=>basename($e->getFile()),'line'=>$e->getLine(),'message_sha256'=>hash('sha256',$e->getMessage())];}
\think\facade\Cache::delete($token);$tokenCleaned=!\think\facade\Cache::has($token);\think\facade\Cache::clear();$after=$snapshot();$retained=[];foreach($ids as $module=>$id){$rows=$read($module);$retained[]=['module'=>$module,'coupon_id'=>$id,'store_id'=>$tenant,'name'=>$rows[0]['name']??null,'is_delete'=>$rows[0]['is_delete']??null];}
$events=[];foreach(file($runtime.'/queries.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)?:[] as $line){$events[]=json_decode($line,true);}$sha=[];foreach(['app/store/model/Coupon.php','app/terminal/model/Coupon.php'] as $f){$sha[$f]=hash_file('sha256',$root.'/'.$f);}
$report=['lane'=>$lane,'cases'=>$cases,'unexpected'=>$error,'before'=>$before,'after'=>$after,'natural_unchanged'=>$before===$after,'retained_owned'=>$retained,'token_cleaned'=>$tokenCleaned,'fixture_visibility_writes'=>$fixtureWrites,'external_attempts'=>is_file($runtime.'/external-private.jsonl')?count(file($runtime.'/external-private.jsonl')):0,'source_sha256'=>$sha,'events'=>$events,'no_new_coupon_insert'=>true,'no_physical_delete'=>true];
foreach(['green-report.json'=>$report,'green-responses.json'=>$responses,'green-source-private.json'=>$source,'fixture_map.json'=>['marker'=>$marker,'tenant'=>$tenant,'coupon_ids'=>$ids]] as $f=>$v){file_put_contents($runtime.'/'.$f,json_encode($v,JSON_UNESCAPED_UNICODE|JSON_PRESERVE_ZERO_FRACTION|JSON_THROW_ON_ERROR));chmod($runtime.'/'.$f,0600);}
echo json_encode(['lane'=>$lane,'cases'=>count($cases),'source_failures'=>count(array_filter($cases,static function($c){return !$c['source_pass'];})),'unexpected'=>$error!==null,'natural_unchanged'=>$before===$after,'retained_owned'=>$retained]),PHP_EOL;exit($error!==null||$before!==$after||!$tokenCleaned?1:0);
