<?php
/** 本机随机前缀隔离表；不修改真实业务数据、不发送外部通知。 */
if(PHP_SAPI!=='cli')exit;
require dirname(__DIR__).'/vendor/autoload.php';
use think\facade\Db;
use think\exception\HttpResponseException;
use app\mobile\service\SuggestionTransfer;
require __DIR__.'/run-scheduled-tasks.php';

class TransferCheck extends \app\mobile\controller\OmsSuggestion {
    public static $account;
    public static $sent=[];
    protected function current(){return self::$account;}
    protected function guarded($action){return $action();}
    protected function notify($row,$title,$recipients){self::$sent[]=[$title,$recipients];}
}
class TransferAdminCheck extends \app\adminVue\controller\V2Suggestion {
    protected function initialize(){$this->actor=['id'=>4,'username'=>'900004','rightid'=>1];}
    protected function requestToken(){return 'isolated-transfer-check';}
}
$app=new \think\App(dirname(__DIR__).'/');$app->initialize();
$config=config('database');$connection=$config['connections'][$config['default']];
if(!in_array($connection['hostname'],['localhost','127.0.0.1'],true))throw new RuntimeException('Local database required');
$original=$connection['prefix'];$worker=($argv[1]??'')==='--worker';
$prefix=$worker?$argv[2]:'transfer_test_'.bin2hex(random_bytes(5)).'_';
if(!preg_match('/^transfer_test_[a-f0-9]{10}_$/D',$prefix))throw new RuntimeException('Invalid test prefix');
$connection['prefix']=$prefix;$connection['fields_cache']=false;
$config['connections']['transfer_test']=$connection;$config['default']='transfer_test';$app->config->set($config,'database');Db::setConfig($app->config);
function transferCheck($ok,$label){if(!$ok)throw new RuntimeException($label);echo 'PASS: '.$label.PHP_EOL;}
function transferAccount($id){TransferCheck::$account=['username'=>$id?'90000'.$id:'900009','admin_id'=>$id,'user_id'=>$id?0:9,'user_type'=>$id?2:1];}
function transferCall($action,$data,$admin=false){
    global $app;$read=in_array($action,['detail','records','options'],true);
    $r=new \think\Request();$r->setMethod($read?'GET':'POST');$r->withGet($read?$data:[]);$r->withPost($read?[]:$data);$r->setAction($action);
    $app->instance('request',$r);$app->instance(\think\Request::class,$r);
    try{$c=$admin?new TransferAdminCheck($app):new TransferCheck($app);$response=$c->$action();}
    catch(HttpResponseException $e){$response=$e->getResponse();}
    return [$response->getCode(),json_decode($response->getContent(),true)];
}
if($worker){transferAccount(2);echo json_encode(transferCall('respondTransfer',['id'=>(int)$argv[3],'transferId'=>(int)$argv[4],'action'=>'accept'])),PHP_EOL;exit;}
function transferInsert($table,$row){
    global $prefix;
    foreach(Db::query('SHOW COLUMNS FROM `'.$prefix.$table.'`') as $field){
        if(!array_key_exists($field['Field'],$row)&&$field['Null']==='NO'&&$field['Default']===null&&$field['Extra']!=='auto_increment')
            $row[$field['Field']]=preg_match('/int|float|decimal|double/',$field['Type'])?0:'';
    }
    return Db::name($table)->insertGetId($row);
}
function newTransferSuggestion(){return transferInsert('onsite_suggestion_list',['eid'=>900009,'ename'=>'提报人','status'=>2,'adopt'=>0,'selfshut'=>1,'owner'=>1,'due_dpt'=>10,'title'=>'交接测试','suggestion'=>'建议内容','reason'=>'','creat_date'=>time(),'feedback_time'=>0]);}
function startTransfer($id){transferAccount(1);$r=transferCall('transfer',['id'=>$id,'ownerId'=>2,'reason'=>'设备改造交接']);if($r[0]!==200)throw new RuntimeException(json_encode($r));return (int)Db::name('onsite_suggestion_list')->where('id',$id)->value('transfer_id');}
$tables=['kpi_record','onsite_suggestion_list','onsite_suggestion_transfer','admin_user','admin_org','onsite_base','member_base','member_list','team_list','auth'];$created=[];
try{
    foreach($tables as $table){Db::execute('CREATE TABLE `'.$prefix.$table.'` LIKE `'.$original.$table.'`');$created[]=$table;}
    Db::execute('ALTER TABLE `'.$prefix.'onsite_suggestion_list` ENGINE=MyISAM');
    foreach([1=>'原负责人',2=>'跨部门接收人',3=>'无关人员',4=>'分配员'] as $id=>$name)transferInsert('admin_user',['id'=>$id,'username'=>'90000'.$id,'truename'=>$name,'dpt'=>$id===2?20:10,'job'=>1,'status'=>1]);
    foreach([10=>'生产部',20=>'设备部'] as $id=>$name)transferInsert('admin_org',['id'=>$id,'mid'=>0,'title'=>$name]);
    transferInsert('member_base',['mid'=>3,'mark'=>'10','title'=>'4']);transferInsert('member_base',['mid'=>3,'mark'=>'20','title'=>'4']);
    transferInsert('onsite_base',['mid'=>88,'content'=>'4']);
    $reject=newTransferSuggestion();Db::name('onsite_suggestion_list')->where('id',$reject)->update(['status'=>1,'owner'=>0,'due_dpt'=>0]);
    transferAccount(4);transferCheck(transferCall('assign',['id'=>$reject,'action'=>'reject','reason'=>'内容重复'])[0]===200,'assignment rejection remains available with MyISAM locks');
    $id=newTransferSuggestion();
    transferAccount(3);transferCheck(transferCall('transfer',['id'=>$id,'ownerId'=>2,'reason'=>'原因'])[0]===403,'unrelated person cannot transfer');
    transferAccount(1);
    foreach([['ownerId'=>1,'reason'=>'原因'],['ownerId'=>999,'reason'=>'原因'],['ownerId'=>2,'reason'=>''],['ownerId'=>2,'reason'=>str_repeat('字',1001)]] as $invalid)
        transferCheck(transferCall('transfer',['id'=>$id]+$invalid)[0]===400,'invalid recipient/reason rejected');
    transferCheck(transferCall('transfer',['id'=>$id,'ownerId'=>2,'canDirectAssign'=>true])[0]===400,'ordinary owner cannot spoof direct assignment');
    $choices=array_column(transferCall('options',[])[1]['data']['owners'],null,'id');transferCheck(isset($choices[2])&&strpos($choices[2]['label'],'设备部')!==false,'cross-department searchable labels available');
    $tid=startTransfer($id);$row=Db::name('onsite_suggestion_list')->find($id);
    transferCheck((int)$row['owner']===1&&(int)$row['due_dpt']===10,'request preserves current owner and department');
    transferCheck(transferCall('transfer',['id'=>$id,'ownerId'=>3,'reason'=>'重复'])[0]===409,'only one pending request');
    transferCheck(transferCall('feedback',['id'=>$id,'adopt'=>1,'reason'=>'措施','dueDate'=>date('Y-m-d'),'mode'=>'save'])[0]===409,'pending transfer blocks feedback');
    transferAccount(2);$detail=transferCall('detail',['id'=>$id])[1]['data'];
    transferCheck($detail['canReceiveTransfer']&&!$detail['canFeedback']&&!$detail['canTransfer'],'receiver has temporary read/respond permissions only');
    $list=transferCall('records',['scope'=>'feedback'])[1]['data']['items'];transferCheck(count($list)===1,'pending transfer included in receiver list');
    $source=new TransferCheck($app);$inbox=new \app\mobile\service\MessageInbox($app);$property=new ReflectionProperty($inbox,'activeSource');$property->setAccessible(true);$property->setValue($inbox,$source);
    $messages=$source->messageItems(TransferCheck::$account,0,$inbox);transferCheck(in_array('transfer-'.$tid,array_column($messages,'node'),true),'receiver gets inbox pending task');
    transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'reject','reason'=>''])[0]===400,'reject requires reason');
    transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'reject','reason'=>'职责不符'])[0]===200,'receiver can reject');
    transferCheck(transferCall('detail',['id'=>$id])[0]===403,'rejection removes temporary access');
    transferAccount(1);$row=Db::name('onsite_suggestion_list')->find($id);
    transferCheck((int)$row['owner']===1&&!(int)$row['transfer_id']&&SuggestionTransfer::history($id)[0]['state']==='rejected','rejection preserves ownership and history');
    $tid=startTransfer($id);transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'cancel'])[0]===200,'initiator can withdraw');
    transferAccount(2);transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'accept'])[0]!==200,'withdrawn request cannot be accepted');
    $tid=startTransfer($id);transferAccount(2);transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'accept'])[0]===200,'cross-department acceptance succeeds');
    $row=Db::name('onsite_suggestion_list')->find($id);transferCheck((int)$row['owner']===2&&(int)$row['due_dpt']===20&&!(int)$row['transfer_id']&&!(int)$row['transfer_to'],'acceptance updates owner/department and clears pending pointers');
    transferCheck(transferCall('respondTransfer',['id'=>$id,'transferId'=>$tid,'action'=>'accept'])[0]===409,'duplicate acceptance rejected');
    transferCheck(transferCall('feedback',['id'=>$id,'adopt'=>1,'reason'=>'已承担改善措施','dueDate'=>date('Y-m-d'),'mode'=>'save'])[0]===200,'accepted owner can save measures');
    $detail=transferCall('detail',['id'=>$id])[1]['data'];transferCheck($detail['statusName']==='进行中'&&!$detail['canTransfer'],'saved measures become ongoing and disable transfer');
    transferCheck(transferCall('transfer',['id'=>$id,'ownerId'=>1,'reason'=>'再转移'])[0]===403,'ongoing transfer is rejected by backend');
    $other=newTransferSuggestion();$tid=startTransfer($other);transferAccount(3);transferCheck(transferCall('respondTransfer',['id'=>$other,'transferId'=>$tid,'action'=>'accept'])[0]===403,'only named receiver may accept');
    $opt=transferCall('options',[],true)[1]['data'];$department=array_column($opt['departments'],null,'id')[20];$before=transferCall('detail',['id'=>$other],true)[1]['data'];
    $payload=['id'=>$other,'departmentId'=>20,'version'=>$before['version'],'departmentVersion'=>$department['version']];
    transferCheck(transferCall('assign',$payload,true)[0]===409,'admin must explicitly confirm cancellation');
    transferCheck(transferCall('assign',$payload+['cancelTransfer'=>true],true)[0]===200,'admin reassignment cancels pending request');
    transferAccount(2);transferCheck(transferCall('respondTransfer',['id'=>$other,'transferId'=>$tid,'action'=>'accept'])[0]!==200,'old request cannot override admin reassignment');
    transferCheck(SuggestionTransfer::history($other)[0]['state']==='cancelled','admin cancellation is recorded');
    // 故意在 MyISAM 写入之后抛错，验证补偿和 InnoDB 回滚。
    $rollback=newTransferSuggestion();$before=Db::name('onsite_suggestion_list')->find($rollback);
    try{SuggestionTransfer::locked(function()use($before){$tid=Db::name('onsite_suggestion_transfer')->insertGetId(['suggestion_id'=>$before['id'],'from_owner'=>1,'to_owner'=>2,'from_name'=>'a','to_name'=>'b','from_department'=>'a','to_department'=>'b','reason'=>'rollback','reply'=>'','state'=>'pending','created_at'=>time()]);SuggestionTransfer::updateSuggestion($before,['owner'=>2,'transfer_id'=>$tid,'transfer_to'=>2]);throw new RuntimeException('injected failure');});}catch(RuntimeException $e){if($e->getMessage()!=='injected failure')throw $e;}
    $row=Db::name('onsite_suggestion_list')->find($rollback);transferCheck((int)$row['owner']===1&&!(int)$row['transfer_id']&&!SuggestionTransfer::history($rollback),'write failure compensates MyISAM and rolls back transfer log');
    // 两个独立 PHP 进程同时接收；表锁只允许其中一次成功。
    $race=newTransferSuggestion();$tid=startTransfer($race);$workers=[];
    for($i=0;$i<2;$i++){$pipes=[];$command=implode(' ',array_map('escapeshellarg',[PHP_BINARY,__FILE__,'--worker',$prefix,(string)$race,(string)$tid]));$process=proc_open($command,[1=>['pipe','w'],2=>['pipe','w']],$pipes,null,null,['bypass_shell'=>true]);$workers[]=[$process,$pipes];}
    $codes=[];foreach($workers as $entry){$output=stream_get_contents($entry[1][1]);$errors=stream_get_contents($entry[1][2]);fclose($entry[1][1]);fclose($entry[1][2]);proc_close($entry[0]);$result=json_decode(trim($output),true);if(!$result)throw new RuntimeException($output.$errors);$codes[]=$result[0];}
    sort($codes);transferCheck($codes===[200,409],'concurrent acceptance commits exactly once');
    $expired=newTransferSuggestion();$tid=startTransfer($expired);
    Db::name('onsite_suggestion_transfer')->where('id',$tid)->update(['created_at'=>time()-86300]);
    transferCheck(SuggestionTransfer::acceptExpired()===0,'transfer does not auto accept before 24 hours');
    Db::name('onsite_suggestion_transfer')->where('id',$tid)->update(['created_at'=>time()-86400]);
    transferAccount(2);$detail=transferCall('detail',['id'=>$expired])[1]['data'];
    transferCheck($detail['owner']===2 && $detail['canFeedback'] && !$detail['pendingTransfer'],'24-hour timeout grants receiver ownership and feedback permission on read');
    $history=SuggestionTransfer::history($expired)[0];
    transferCheck($history['state']==='accepted' && $history['actor_id']===0 && strpos($history['reply'],'自动同意')!==false,'automatic acceptance records system actor and reason');
    transferCheck(SuggestionTransfer::acceptExpired()===0,'repeated timeout processing is idempotent');
    transferCheck(transferCall('respondTransfer',['id'=>$expired,'transferId'=>$tid,'action'=>'reject','reason'=>'迟到拒绝'])[0]===409,'late rejection cannot undo automatic acceptance');
    $invalid=newTransferSuggestion();$tid=startTransfer($invalid);
    Db::name('onsite_suggestion_transfer')->where('id',$tid)->update(['created_at'=>time()-86401]);
    Db::name('admin_user')->where('id',2)->update(['status'=>0]);
    SuggestionTransfer::acceptExpired();$row=Db::name('onsite_suggestion_list')->find($invalid);
    transferCheck((int)$row['owner']===1 && !(int)$row['transfer_id'] && SuggestionTransfer::history($invalid)[0]['state']==='cancelled','inactive receiver cancels expired request and frees original owner');
    Db::name('admin_user')->where('id',2)->update(['status'=>1]);
    // 部门窗口以实际责任部门的配置判定；只在本人仍负责且尚未反馈时免确认。
    Db::name('member_base')->where(['mid'=>3,'mark'=>'10'])->update(['title'=>'1']);
    $direct=newTransferSuggestion();transferAccount(1);
    $detail=transferCall('detail',['id'=>$direct])[1]['data'];transferCheck($detail['canDirectAssign'],'current department window receives direct assignment permission');
    $sent=count(TransferCheck::$sent);
    transferCheck(transferCall('transfer',['id'=>$direct,'ownerId'=>2])[0]===200,'department window assigns without reason or acceptance');
    $row=Db::name('onsite_suggestion_list')->find($direct);$history=SuggestionTransfer::history($direct);
    transferCheck((int)$row['owner']===2&&(int)$row['due_dpt']===20&&!(int)$row['transfer_id']&&!(int)$row['transfer_to'],'direct assignment immediately changes owner and department without pending request');
    transferCheck($history[0]['state']==='assigned'&&$history[0]['reason']===''&&$history[0]['actor_id']===1&&$history[0]['handled_at']>0,'direct assignment keeps actor and time history without fabricated reason');
    transferCheck(count(TransferCheck::$sent)===$sent+1&&TransferCheck::$sent[$sent][1]===[2]&&strpos(TransferCheck::$sent[$sent][0],'待接收')===false,'assigned owner receives feedback task notification instead of acceptance request');
    transferAccount(2);$detail=transferCall('detail',['id'=>$direct])[1]['data'];transferCheck($detail['canFeedback']&&!$detail['canReceiveTransfer']&&!$detail['canDirectAssign'],'ordinary assigned person may feedback but cannot directly reassign');
    transferCheck(transferCall('transfer',['id'=>$direct,'ownerId'=>3])[0]===400,'ordinary assigned person still must provide reason');
    transferAccount(1);transferCheck(transferCall('transfer',['id'=>$direct,'ownerId'=>3])[0]===403,'former department window cannot override the new owner');
    $ongoing=newTransferSuggestion();Db::name('onsite_suggestion_list')->where('id',$ongoing)->update(['adopt'=>1,'reason'=>'已经保存措施','feedback_time'=>time()]);
    transferCheck(transferCall('transfer',['id'=>$ongoing,'ownerId'=>2])[0]===403,'department window cannot assign ongoing suggestions');
    $cutoff=strtotime('today')-2*86400;
    $old=transferInsert('kpi_record',['kpi_date'=>$cutoff-1,'status'=>1,'update'=>0]);
    $boundary=transferInsert('kpi_record',['kpi_date'=>$cutoff,'status'=>1,'update'=>0]);
    $refused=transferInsert('kpi_record',['kpi_date'=>$cutoff-1,'status'=>2,'update'=>0]);
    transferCheck(scheduledKpiConfirm()===1,'unified scheduler confirms only overdue pending KPI records');
    $kpi=Db::name('kpi_record')->find($old);
    transferCheck((int)$kpi['status']===4 && (int)$kpi['update']>0,'KPI auto confirmation records result time for inbox');
    transferCheck((int)Db::name('kpi_record')->where('id',$boundary)->value('status')===1,'KPI keeps strict original date boundary');
    transferCheck((int)Db::name('kpi_record')->where('id',$refused)->value('status')===2 && scheduledKpiConfirm()===0,'refused KPI remains unchanged and repeated scheduling is idempotent');
    $engine=Db::query('SHOW TABLE STATUS WHERE Name = ?',[$prefix.'onsite_suggestion_list']);transferCheck($engine[0]['Engine']==='MyISAM','original suggestion engine remains MyISAM');
    echo "All suggestion transfer checks passed.\n";
}finally{
    foreach(array_reverse($created) as $table)Db::execute('DROP TABLE IF EXISTS `'.$prefix.$table.'`');
}
