<?php
declare(strict_types=1);
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');$dm=$lane==='C';$marker='CDXCR10A6C8D219F037';
if(!in_array($lane,['B','C'],true)||$base!==($dm?'http://127.0.0.1:18352':'http://127.0.0.1:18351')){exit(78);}
$stage='preflight';$cases=[];$responses=[];$source=[];$fixtureDml=[];$cleanup=[];$couponId=0;$claimId=0;$unexpected=null;$token=bin2hex(random_bytes(24));
$ai=cpPdo();$store=cpPdo(true);$actors=cpActors($ai,$store,$lane);$actor=$actors['actor_id'];$before=cpTableSnapshot($ai);
if(cpRows($ai,'SELECT coupon_id FROM yoshop_coupon WHERE name=?',[$marker])||cpRows($ai,'SELECT user_coupon_id FROM yoshop_user_coupon WHERE user_id=? AND store_id=10001',[$actor])){throw new RuntimeException('Owned marker or actor claims not empty');}
$app=cpBootstrap($root,$runtime);$ormUser=\app\api\model\User::detail($actor);if(empty($ormUser)||(int)$ormUser['user_id']!==$actor){throw new RuntimeException('Actual auth actor gate');}
$ownCoupon=static function()use($ai,$marker,&$couponId,$dm):array{
    $cast=$dm?'VARCHAR(64)':'CHAR';$extra=[];foreach(['reduce_price','min_price','write_off_limit_amount'] as $f){$extra[]='CAST('.$f.' AS '.$cast.') AS exact_'.$f;}
    $rows=cpRows($ai,'SELECT c.*,'.implode(',',$extra).' FROM yoshop_coupon c WHERE coupon_id=? AND name=? AND store_id=10001',[$couponId,$marker]);foreach($rows as &$row){foreach(['reduce_price','min_price','write_off_limit_amount'] as $f){$v=$row['exact_'.$f];if(!is_string($v)||!preg_match('/^-?[0-9]+(?:\.[0-9]+)?$/D',$v)){throw new RuntimeException('Exact source decimal text required');}$row['exact_'.$f]=bcadd($v,'0',2);}}unset($row);return $rows;
};
$ownClaims=static function()use($ai,$marker,$actor,&$couponId,$dm):array{
    $cast=$dm?'VARCHAR(64)':'CHAR';$rows=cpRows($ai,'SELECT uc.*,CAST(reduce_price AS '.$cast.') AS exact_reduce_price,CAST(min_price AS '.$cast.') AS exact_min_price FROM yoshop_user_coupon uc WHERE coupon_id=? AND user_id=? AND name=? AND store_id=10001',[$couponId,$actor,$marker]);foreach($rows as &$row){foreach(['reduce_price','min_price'] as $f){$v=$row['exact_'.$f];if(!is_string($v)||!preg_match('/^-?[0-9]+(?:\.[0-9]+)?$/D',$v)){throw new RuntimeException('Exact source decimal text required');}$row['exact_'.$f]=bcadd($v,'0',2);}}unset($row);return $rows;
};
$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;
};
$wire=static function($v){return json_decode(json_encode($v,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR));};
$expectedRow=static function(array $row,bool $userCoupon,bool $received)use($wire){
    $hide=$userCoupon?['store_id','create_time','update_time']:['total_num','receive_num','status','is_delete','store_id','create_time','update_time'];
    $out=$row;foreach($hide as $f){unset($out[$f]);}foreach(array_keys($out) as $f){if(strpos($f,'exact_')===0){unset($out[$f]);}}
    foreach(['reduce_price','min_price'] as $f){$out[$f]=$row['exact_'.$f];}if(!$userCoupon){$out['write_off_limit_amount']=$row['exact_write_off_limit_amount'];}
    $out['discount']=$row['discount']/10;$out['start_time']=$row['start_time']?date('Y/m/d',(int)$row['start_time']):($userCoupon?'1970/01/01':null);$out['end_time']=$row['end_time']?date('Y/m/d',(int)$row['end_time']):($userCoupon?'1970/01/01':null);
    $out['apply_range_config']=$row['apply_range_config']?json_decode($row['apply_range_config'],true):[];
    if(!$userCoupon){$out['is_receive']=$received;$out['state']=['text'=>$received?'已领取':'正常','value'=>$received?0:1];}else{$out['state']=['text'=>'正常','value'=>1];}
    return $wire($out);
};
$request=static function(string $name,string $method,string $path,array $args=[])use($base,$token,&$responses):array{
    $params=['storeId'=>10001]+$args;$url=$base.$path.($method==='GET'?'?'.http_build_query($params):'');$options=['method'=>$method,'header'=>"Host: cdx-coupon-retest.invalid\r\nAccess-Token: ".$token."\r\nplatform: MP-WEIXIN\r\nversion: 2.0.51\r\nenv-version: develop\r\nX-Pilot-Scenario: PILOT_CPN_".$name."\r\n",'ignore_errors'=>true,'follow_location'=>0,'timeout'=>25];
    if($method==='POST'){$options['header'].="Content-Type: application/x-www-form-urlencoded\r\n";$options['content']=http_build_query($params);}
    $raw=file_get_contents($url,false,stream_context_create(['http'=>$options]));preg_match('~^HTTP/\S+ (\d+)~',$http_response_header[0]??'',$match);$body=json_decode((string)$raw);
    $result=['http_status'=>(int)($match[1]??0),'body'=>$body,'wire_sha256'=>hash('sha256',(string)$raw),'method'=>$method,'path'=>$path];$responses[$name]=$result;return $result;
};
$record=static function(string $name,array $r,bool $pass,string $comparison)use(&$cases):void{$trace=$r['body']->trace_id??null;$cases[]=['case'=>$name,'method'=>$r['method'],'path'=>$r['path'],'http_status'=>$r['http_status'],'business_status'=>$r['body']->status??null,'source_pass'=>$pass,'trace_contract'=>is_string($trace)&&preg_match('/^[a-f0-9]{13}$/D',$trace)===1,'comparison'=>$comparison];};
$readCoupon=static function(string $name,bool $received)use($request,$ownCoupon,$expectedRow,$same,$record,&$source,&$responses,&$couponId):void{
    $r=$request($name,'GET','/api/Coupon/list');$rows=$ownCoupon();$list=$r['body']->data->list??null;$own=[];if(is_array($list)){foreach($list as $item){if(is_object($item)&&($item->coupon_id??null)===$couponId){$own[]=$item;}}}
    $ok=$r['http_status']===200&&($r['body']->status??null)===200&&count($rows)===1&&count($own)===1&&$same($expectedRow($rows[0],false,$received),$own[0]);
    $responses[$name]['owned_row']=$own[0]??null;$responses[$name]['natural_count']=is_array($list)?count($list)-count($own):null;
    $fields=[];if($rows){foreach(['unavailable_dates','unavailable_time_ranges'] as $field){$v=$rows[0][$field];$fields[$field]=['type'=>gettype($v),'bytes'=>$v===null?null:strlen($v),'sha256'=>$v===null?null:hash('sha256',$v)];}}
    $source[$name]=['owned_count'=>count($rows),'expected_wire'=>$rows?$expectedRow($rows[0],false,$received):null,'text_fields'=>$fields];$record($name,$r,$ok,'owned_coupon_full_row_only_natural_page_not_equal');
};
$readMine=static function(string $name,array $args,bool $expectOwn)use($request,$ownClaims,$expectedRow,$same,$record,&$source,&$responses,&$claimId,&$couponId):void{
    $r=$request($name,'GET','/api/MyCoupon/list',$args);$rows=$ownClaims();$list=$r['body']->data->list->data??null;$total=$r['body']->data->list->total??null;
    $ok=$r['http_status']===200&&($r['body']->status??null)===200&&is_array($list)&&$total===($expectOwn?1:0)&&count($list)===($expectOwn?1:0);
    if($expectOwn){$ok=$ok&&count($rows)===1&&($list[0]->user_coupon_id??null)===$claimId&&($list[0]->coupon_id??null)===$couponId&&$same($expectedRow($rows[0],true,true),$list[0]);foreach(['employee_id','issue_rule_id','issue_record_id'] as $f){$ok=$ok&&is_int($list[0]->$f??null);}}
    $source[$name]=['owned_count'=>count($rows),'expected_wire'=>$expectOwn&&$rows?$expectedRow($rows[0],true,true):null];$record($name,$r,$ok,'full_response_after_owned_id_and_trace_mapping');
};
try{
    $stage='boundary';$boundary=json_decode(file_get_contents($base.'/__poc_boundary'),true);if(empty($boundary['coupon_allowlist'])||empty($boundary['external_stubs'])||empty($boundary['new_private_root'])||empty($boundary['file_cache'])){throw new RuntimeException('Private boundary absent');}
    \think\facade\Cache::set($token,['user'=>$ormUser,'store_id'=>10001,'is_login'=>true,'password_version'=>$actors['password_version'],'login_type'=>'sms'],3600);if(!\think\facade\Cache::has($token)){throw new RuntimeException('Token absent');}
    file_put_contents($runtime.'/fixture_map.json',json_encode(['marker'=>$marker,'actor_id'=>$actor,'coupon_id'=>0,'claim_id'=>0,'token'=>$token]));chmod($runtime.'/fixture_map.json',0600);
    $readMine('MY_BEFORE',[],false);
    $stage='prepare_owned_coupon';$desc=$dm?'"DESCRIBE"':'`describe`';$sql='INSERT INTO yoshop_coupon (name,coupon_type,reduce_price,discount,min_price,expire_type,expire_day,start_time,end_time,apply_range,apply_range_config,total_num,receive_num,'.$desc.',status,sort,is_delete,store_id,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
    $s=$ai->prepare($sql);$s->execute([$marker,10,'12.34',0,'56.78',20,0,strtotime('2020-01-01 00:00:00'),strtotime('2030-12-31 00:00:00'),10,'[]',10,0,'CDX isolated compatibility test only',1,0,0,10001,strtotime('2020-01-01 00:00:00'),strtotime('2020-01-01 00:00:00')]);$fixtureDml[]=['operation'=>'insert','table'=>'coupon','rows'=>$s->rowCount()];
    $found=cpRows($ai,'SELECT coupon_id FROM yoshop_coupon WHERE name=? AND store_id=10001',[$marker]);if(count($found)!==1){throw new RuntimeException('Prepared coupon ownership');}$couponId=(int)$found[0]['coupon_id'];
    $map=json_decode(file_get_contents($runtime.'/fixture_map.json'),true);$map['coupon_id']=$couponId;file_put_contents($runtime.'/fixture_map.json',json_encode($map));
    $defaults=$ownCoupon();if(count($defaults)!==1||$defaults[0]['exact_write_off_limit_amount']!=='0.00'||$defaults[0]['unavailable_dates']!==null||$defaults[0]['unavailable_time_ranges']!==null){throw new RuntimeException('Source coupon defaults failed');}
    $readCoupon('CP_DEFAULT',false);
    $stage='actual_receive';$r=$request('RECEIVE','POST','/api/MyCoupon/receive',['couponId'=>$couponId]);$claims=$ownClaims();$coupons=$ownCoupon();
    $ok=$r['http_status']===200&&($r['body']->status??null)===200&&($r['body']->message??null)==='领取成功'&&count($claims)===1&&count($coupons)===1&&(int)$coupons[0]['receive_num']===1;
    if($claims){$claimId=(int)$claims[0]['user_coupon_id'];foreach(['employee_id','issue_rule_id','issue_record_id'] as $f){$ok=$ok&&$claims[0][$f]===0;}}
    $record('RECEIVE',$r,$ok,'receipt_source_one_row_and_receive_num_one');if(!$ok){throw new RuntimeException('Receive failed; later claim tests unsafe');}
    $map['claim_id']=$claimId;file_put_contents($runtime.'/fixture_map.json',json_encode($map));$readMine('MY_DEFAULT',[],true);
    $beforeDup=['coupon'=>cpDigest($ownCoupon()),'claims'=>cpDigest($ownClaims())];$r=$request('REPEAT','POST','/api/MyCoupon/receive',['couponId'=>$couponId]);$afterDup=['coupon'=>cpDigest($ownCoupon()),'claims'=>cpDigest($ownClaims())];
    $record('REPEAT',$r,$r['http_status']===200&&($r['body']->status??null)===500&&($r['body']->message??null)==='当前用户已领取该优惠券'&&$beforeDup===$afterDup,'repeat_business_reject_and_no_source_change');$readCoupon('CP_CLAIMED',true);
    $readMine('MY_USABLE',['dataType'=>'isUsable'],true);$readMine('MY_LOW_AMOUNT',['dataType'=>'isUsable','amount'=>'12.34'],false);$readMine('MY_THRESHOLD',['dataType'=>'isUsable','amount'=>'56.78'],true);
    $stage='owned_ids';foreach(['MY_IDS_POSITIVE'=>[101,102,103],'MY_IDS_RESTORED'=>[0,0,0]] as $name=>$ids){$s=$ai->prepare('UPDATE yoshop_user_coupon SET employee_id=?,issue_rule_id=?,issue_record_id=? WHERE user_coupon_id=? AND coupon_id=? AND user_id=? AND name=? AND store_id=10001');$s->execute(array_merge($ids,[$claimId,$couponId,$actor,$marker]));$fixtureDml[]=['operation'=>'update','table'=>'user_coupon','rows'=>$s->rowCount()];$raw=$ownClaims();if(count($raw)!==1||[$raw[0]['employee_id'],$raw[0]['issue_rule_id'],$raw[0]['issue_record_id']]!==$ids){throw new RuntimeException('Owned temporary IDs source mismatch');}$readMine($name,[],true);}
    $stage='clob_boundaries';$small=json_encode(['中文"引号',"单'引号",'2026-10-10'],JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR);
    foreach(['CP_JSON_QUOTES'=>$small,'CP_JSON_32767'=>'["'.str_repeat('A',32763).'"]','CP_JSON_32768'=>'["'.str_repeat('B',32764).'"]'] as $name=>$json){$s=$ai->prepare('UPDATE yoshop_coupon SET write_off_limit_amount=?,unavailable_dates=?,unavailable_time_ranges=? WHERE coupon_id=? AND name=? AND store_id=10001');$s->execute(['1234.56',$json,$json,$couponId,$marker]);$fixtureDml[]=['operation'=>'update','table'=>'coupon','rows'=>$s->rowCount()];$raw=$ownCoupon();if(count($raw)!==1||$raw[0]['exact_write_off_limit_amount']!=='1234.56'||$raw[0]['unavailable_dates']!==$json||$raw[0]['unavailable_time_ranges']!==$json){throw new RuntimeException('CLOB parameter source mismatch');}$readCoupon($name,true);}
}catch(Throwable $e){$unexpected=['stage'=>$stage,'class'=>get_class($e),'file'=>basename($e->getFile()),'line'=>$e->getLine(),'message_sha256'=>hash('sha256',$e->getMessage())];}
finally{
    if($ai->inTransaction()){$ai->rollBack();}
    if($couponId>0){foreach($ownClaims() as $row){$s=$ai->prepare('DELETE FROM yoshop_user_coupon WHERE user_coupon_id=? AND coupon_id=? AND user_id=? AND name=? AND store_id=10001');$s->execute([(int)$row['user_coupon_id'],$couponId,$actor,$marker]);$cleanup['user_coupon']=($cleanup['user_coupon']??0)+$s->rowCount();}
        foreach($ownCoupon() as $row){$s=$ai->prepare('DELETE FROM yoshop_coupon WHERE coupon_id=? AND name=? AND store_id=10001');$s->execute([$couponId,$marker]);$cleanup['coupon']=($cleanup['coupon']??0)+$s->rowCount();}}
}
if($unexpected===null){$readMine('MY_CLEANED',[],false);$r=$request('CP_CLEANED','GET','/api/Coupon/list');$list=$r['body']->data->list??null;$own=[];if(is_array($list)){foreach($list as $v){if(($v->coupon_id??null)===$couponId){$own[]=$v;}}}$record('CP_CLEANED',$r,$r['http_status']===200&&($r['body']->status??null)===200&&is_array($list)&&!$own,'owned_absent_natural_list_retained');}
\think\facade\Cache::delete($token);$tokenCleaned=!\think\facade\Cache::has($token);\think\facade\Cache::clear();$audit=cpPdo();$remaining=['coupon'=>count(cpRows($audit,'SELECT coupon_id FROM yoshop_coupon WHERE coupon_id=? AND name=? AND store_id=10001',[$couponId,$marker])),'user_coupon'=>count(cpRows($audit,'SELECT user_coupon_id FROM yoshop_user_coupon WHERE coupon_id=? AND user_id=? AND name=? AND store_id=10001',[$couponId,$actor,$marker]))];
$after=cpTableSnapshot($audit);$actorsAfter=cpActors($audit,cpPdo(true),$lane);$external=is_file($runtime.'/external-private.jsonl')?count(file($runtime.'/external-private.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES)):0;
$events=[];if(is_file($runtime.'/queries.jsonl')){foreach(file($runtime.'/queries.jsonl',FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line){$events[]=json_decode($line,true);}}
$sourceSha=[];foreach(['app/api/controller/Coupon.php','app/api/controller/MyCoupon.php','app/api/model/Coupon.php','app/api/model/UserCoupon.php','app/common/model/Coupon.php','app/common/model/UserCoupon.php','app/api/model/User.php','app/common/database/Dm.php','app/common/database/DmBuilder.php','cores/BaseModel.php'] as $f){$sourceSha[$f]=hash_file('sha256',$root.'/'.$f);}
$report=['lane'=>$lane,'cases'=>$cases,'unexpected'=>$unexpected,'before'=>$before,'after'=>$after,'natural_unchanged'=>$before===$after,'actors_unchanged'=>$actors===$actorsAfter,'cleanup'=>$cleanup,'independent_remaining'=>$remaining,'token_cleaned'=>$tokenCleaned,'external_attempts'=>$external,'fixture_dml'=>$fixtureDml,'source_sha256'=>$sourceSha,'events'=>$events,'input_limit_note'=>'parameter-bound <=32768 bytes; not SQL-literal or bulk-migration proof','four_byte_note'=>'known unresolved MySQL charset issue; not exercised/masked','issuance_note'=>'positive three IDs are temporary owned row values, not issuance business'];
foreach(['report.json'=>$report,'responses.json'=>$responses,'source-private.json'=>$source,'fixture_map.json'=>['marker'=>$marker,'coupon_id'=>$couponId,'claim_id'=>$claimId,'actor_id'=>$actor]] as $file=>$data){file_put_contents($runtime.'/'.$file,json_encode($data,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRESERVE_ZERO_FRACTION|JSON_THROW_ON_ERROR));chmod($runtime.'/'.$file,0600);}
echo json_encode(['lane'=>$lane,'cases'=>count($cases),'source_failures'=>count(array_filter($cases,static function($c){return !$c['source_pass'];})),'unexpected'=>$unexpected!==null,'natural_unchanged'=>$before===$after,'actors_unchanged'=>$actors===$actorsAfter,'remaining'=>$remaining,'external_attempts'=>$external]),PHP_EOL;
exit($unexpected!==null||$before!==$after||$actors!==$actorsAfter||array_sum($remaining)||!$tokenCleaned||$external?1:0);
