<?php
/** Isolated front migration acceptance. Does NOT bootstrap web routes, queues or Redis. */
declare(strict_types=1);
namespace app\p\logic {
    // Boundary interception: never delegate to global network/log/sign helpers.
    function getCommonSign($param, $key, $a, $b) {
        if (!isset($GLOBALS['front_zhipan_boundary'])) throw new \RuntimeException('Unexpected signature boundary invocation');
        $GLOBALS['front_zhipan_boundary']['signature_calls']++;
        $GLOBALS['front_zhipan_boundary']['signature_params']=$param;
        $GLOBALS['front_zhipan_boundary']['signature_key']=$key;
        return 'isolated-front-contract-signature';
    }
    function writeLog($value) {
        if (!isset($GLOBALS['front_zhipan_boundary'])) throw new \RuntimeException('Unexpected intercepted log invocation');
        $GLOBALS['front_zhipan_boundary']['log_calls']++;
        // Deliberately discard potentially private URL/payload contents.
    }
    function http_post($param, $url) {
        if (!isset($GLOBALS['front_zhipan_boundary'])) throw new \RuntimeException('Unexpected HTTP boundary invocation');
        $GLOBALS['front_zhipan_boundary']['http_stub_calls']++;
        $GLOBALS['front_zhipan_boundary']['http_params']=$param;
        $GLOBALS['front_zhipan_boundary']['http_url']=$url;
        return '{"code":1}';
    }
}
namespace app\api\service {function date($format,$timestamp=null){return \date($format,$timestamp===null?($GLOBALS['sign_clock']??time()):$timestamp);}}
namespace app\common {function date($format,$timestamp=null){return \date($format,$timestamp===null?($GLOBALS['sign_clock']??time()):$timestamp);}}
namespace app\common\service {function date($format,$timestamp=null){return \date($format,$timestamp===null?($GLOBALS['sign_clock']??time()):$timestamp);}}
namespace {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
ini_set('display_errors', '0');
date_default_timezone_set('Asia/Shanghai');
if ($argc !== 3) { fwrite(STDERR, "Usage: php two_menu_real_dm.php private-config.json report.json\n"); exit(2); }
$cfg = json_decode(file_get_contents($argv[1]), true);
$reportPath = $argv[2];
$report = ['kind'=>'two-menu-real-dm-v2', 'started'=>date(DATE_ATOM), 'checks'=>[], 'external_http_performed'=>false, 'redis_used'=>false, 'production_target_allowed'=>false];
class FrontAcceptanceFailure extends RuntimeException {}
function ensure($ok, string $message): void { if (!$ok) throw new FrontAcceptanceFailure($message); }
function checked(string $name, string $source, callable $fn): void {
    global $report;
    try { $detail=$fn(); $report['checks'][]=['name'=>$name,'source'=>$source,'status'=>'pass','detail'=>$detail]; }
    catch (Throwable $e) { $report['checks'][]=['name'=>$name,'source'=>$source,'status'=>'fail','error_class'=>get_class($e),'error_code'=>(string)$e->getCode(),'message'=> $e instanceof FrontAcceptanceFailure ? substr($e->getMessage(),0,180) : 'Runtime/SQL failure; inspect isolated task locally']; }
}
function skipCheck(string $name, string $why): void { global $report; $report['checks'][]=['name'=>$name,'status'=>'skip','reason'=>$why]; }
try {
    ensure(is_array($cfg) && ($cfg['isolated']??false)===true,'Explicit isolated=true required');
    $schema=(string)($cfg['schema']??$cfg['user']??'');
    ensure(preg_match('/^(?:FRONT|FRT|FR_|ZWX)[A-Z0-9_]*$/D',$schema)===1,'Dedicated front rehearsal schema required');
    ensure(($cfg['host']??'')==='127.0.0.1' && (int)($cfg['port']??0)>1024 && (int)$cfg['port']!==5236,'Only isolated loopback non-production port');
    ensure((int)$cfg['port']===15236 && ($cfg['expectedInstance']??'')==='FS1R20261021','Only approved isolated front-sign instance');
    ensure(($cfg['user']??'')===$schema,'Business user must equal dedicated schema');
    ensure(in_array($cfg['business']??'', ['zhct_zywxb','zhct_zywxb2'],true),'Explicit front business required');
    ensure(!empty($cfg['expectedInstance']) && !empty($cfg['expectedBuild']),'Exact fingerprint required');
    $source=realpath($cfg['sourceRoot']??'');
    ensure($source!==false && is_file($source.'/thinkphp/base.php'),'Missing frozen store root');
    $runtime=realpath($cfg['runtimeRoot']??'');
    ensure($runtime!==false && $runtime!==$source && strpos($runtime,$source.'/')!==0,'Use existing external runtimeRoot');
    ensure(!is_file($runtime.'/.env'),'Runtime .env must not override private config');
    ensure(in_array('dm', PDO::getAvailableDrivers(),true),'PDO_DM unavailable');
    // Fingerprint before framework or any write-capable method.
    $pdo=new PDO('dm:host='.$cfg['host'].';port='.(int)$cfg['port'].';schema='.$schema,$cfg['user'],$cfg['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_CASE=>PDO::CASE_LOWER]);
    $fp=$pdo->query("SELECT ID_CODE AS build, USER AS username, SF_GET_CASE_SENSITIVE_FLAG() AS cs, SF_GET_UNICODE_FLAG() AS unicode_flag, PAGE() AS page_bytes FROM DUAL")->fetch(PDO::FETCH_ASSOC);
    $ini=[];foreach($pdo->query("SELECT PARA_NAME,PARA_VALUE FROM SYS.V\$DM_INI WHERE PARA_NAME IN ('INSTANCE_NAME','PORT_NUM','COMPATIBLE_MODE')")->fetchAll(PDO::FETCH_ASSOC)as $r)$ini[$r['para_name']]=trim((string)$r['para_value']);
    ensure($fp['build']===$cfg['expectedBuild'] && strtoupper($fp['username'])===$schema && $ini['INSTANCE_NAME']===$cfg['expectedInstance'] && (int)$ini['PORT_NUM']===(int)$cfg['port'] && $ini['COMPATIBLE_MODE']==='4' && (int)$fp['cs']===0 && (int)$fp['unicode_flag']===1 && (int)$fp['page_bytes']===32768,'Target fingerprint mismatch');
    ensure(in_array($schema,['FRONT24_SIGN1','FRONT24_SIGN2'],true),'Dedicated synthetic signs schema only');
    $report['schema']=$schema; $report['build']=$fp['build'];
    define('BUSINESS',$cfg['business']);define('TP_ENV','prod');define('ROOT_PATH',$runtime.'/');define('APP_PATH',$source.'/application/');define('VENDOR_PATH',$source.'/vendor/');define('EXTEND_PATH',$source.'/extend/');define('RUNTIME_PATH',$runtime.'/runtime/');
    require $source.'/thinkphp/base.php';
    \think\Loader::addNamespace('app',$source.'/application/');
    require $source.'/thinkphp/helper.php';
    require $source.'/application/common.php';
    \think\Config::set('database',['type'=>\app\cores\database\Dm::class,'hostname'=>$cfg['host'],'hostport'=>(int)$cfg['port'],'database'=>$schema,'username'=>$cfg['user'],'password'=>$cfg['password'],'prefix'=>'ydy_','dsn'=>'','params'=>[],'debug'=>false,'ai_zhct'=>['enabled'=>false]]);
    \think\Config::set('log',['type'=>'File','path'=>$runtime.'/log/']);\think\Config::set('cache',['type'=>'File','path'=>$runtime.'/cache/']);\think\Config::set('session',['type'=>'','auto_start'=>false]);
    \think\Config::set('zywxb',['enabled'=>true,'fixed_user_uuid'=>$cfg['fixedUserUuid']??'39DCAD32-991D-E7E8-300C-6416E153A0FB']);
    \think\Config::set('meal_type',3);
    $db=\think\Db::connect();
    ensure($db instanceof \app\cores\database\Dm,'Actual application DM adapter required');
    $expectedTables=['ydy_config','ydy_dishes','ydy_dishes_tag','ydy_electronic_menu','ydy_electronic_menu_bind_code','ydy_electronic_menu_device','ydy_electronic_menu_device_log','ydy_equipment','ydy_plate_dishes','ydy_recipes','ydy_restaurants','ydy_upload_file'];$actualTables=array_map('strtolower',$db->getTables());sort($expectedTables);sort($actualTables);ensure($actualTables===$expectedTables,'Exact 12 fixture table names required');
    $report['scope']=['real_dm_service_calls'=>true,'new_controller_menu_authorization_tested'=>false,'old_model_cross_store_isolation_tested'=>false,'concurrent_activation_tested'=>false,'http_image_or_screen_tested'=>false];
    $today=date('Y-m-d');$GLOBALS['sign_clock']=strtotime($today.' 07:30:00');
    $seedPath=$runtime.'/seed.json';
    if(!is_file($seedPath)){
      $q=function($table,$row){return (int)\think\Db::name($table)->insertGetId($row);};
      $rest=['uuid'=>guid(),'name'=>'Synthetic-A','book_rules'=>'{}','order_rules'=>'{}','is_delete'=>2,'morning_start'=>'00:00','morning_end'=>'08:59','noon_start'=>'09:00','noon_end'=>'14:59','night_start'=>'15:00','night_end'=>'19:29'];
      $seed=['restaurant'=>$q('restaurants',$rest)];$rest['uuid']=guid();$rest['name']='Synthetic-B';$seed['restaurant2']=$q('restaurants',$rest);
      foreach(['ossDisjunctor'=>'0','ossAccessKeyId'=>'','ossAccessKeySecret'=>'','ossEndpoint'=>'','ossBucket'=>'','ossHost'=>'http://127.0.0.1:18088','businessIdentify'=>''] as $k=>$v)$q('config',['config_key'=>$k,'config_value'=>$v,'status'=>1]);
      foreach([1,2,3,4] as $meal)$seed['files'][$meal]=$q('upload_file',['type'=>1,'file_name'=>'synthetic-'.$meal.'.jpg','file_path'=>'synthetic/'.$meal.'.jpg']);
      $seed['equipment']=$q('equipment',['code'=>'synthetic-old-plate','name'=>'Synthetic Plate','type'=>8,'del_flag'=>0,'restaurant_id'=>$seed['restaurant'],'create_time'=>$today.' 00:00:00','update_time'=>$today.' 00:00:00','create_by'=>'synthetic','update_by'=>'synthetic']);
      $seed['dish_uuid']=guid();$seed['dish']=$q('dishes',['uuid'=>$seed['dish_uuid'],'name'=>'Synthetic Dish','price'=>'13.80','file_path'=>'synthetic/dish.jpg','carbohydrate'=>'8.25','protein'=>'22.50','fat'=>'11.75','spicy_level'=>2,'status'=>1,'is_del'=>1]);
      $q('dishes_tag',['dishes_id'=>$seed['dish'],'name'=>'Synthetic tag','is_delete'=>0]);
      foreach([1,2,3,4] as $meal)$q('recipes',['uuid'=>guid(),'dishes_uuid'=>$seed['dish_uuid'],'menu_date'=>$today,'create_time'=>$today.' 00:00:00','create_by'=>'synthetic','meal_times'=>$meal,'type'=>1,'restaurant_id'=>$seed['restaurant']]);
      file_put_contents($seedPath,json_encode($seed));chmod($seedPath,0600);
    }else{$seed=json_decode(file_get_contents($seedPath),true);}
    function rejects(callable $fn,string $label,string $expected):void{try{$fn();}catch(\Throwable $e){ensure(get_class($e)===\RuntimeException::class,'Expected exact business RuntimeException, not PDO/assertion failure');ensure($e->getMessage()===$expected,'Unexpected business rejection');return;}throw new FrontAcceptanceFailure($label);}
    function legacyRejects(callable $fn):void{try{$fn();}catch(\Throwable $e){ensure(get_class($e)===\app\cores\exception\BaseException::class && $e->getMessage()==='设备不存在','Wrong legacy rejection class/message');return;}throw new FrontAcceptanceFailure('Legacy invalid device accepted');}

    checked('utf8_real_database_bytes','PHP PDO_DM insert/read plus server HEX oracle',function(){
      $value='中文菜牌𠮷😀';$id=\think\Db::name('upload_file')->insertGetId(['file_name'=>$value,'file_path'=>'synthetic/unicode.png','type'=>1]);
      $row=\think\Db::name('upload_file')->where('id',$id)->field('file_name,HEX(file_name) AS server_hex')->find();
      ensure($row['file_name']===$value&&strtoupper($row['server_hex'])===strtoupper(bin2hex($value)),'UTF8 client/server mismatch hidden by roundtrip');
      ensure(json_encode($row,JSON_UNESCAPED_UNICODE)!==false,'invalid JSON');return ['chinese'=>true,'supplementary_unicode'=>true,'server_bytes_equal'=>true];
    });
    $legacy=new \app\p\logic\Equipment();$plate=new \app\api\service\Plate();
    $params=['equipment_id'=>$seed['equipment'],'menu_date'=>$today,'morning_file_id'=>$seed['files'][1],'noon_file_id'=>$seed['files'][2],'night_file_id'=>$seed['files'][3],'night_snack_file_id'=>$seed['files'][4]];
    checked('legacy_config_save','Equipment::plateUpdate',function()use($legacy,$params){$x=$legacy->plateUpdate($params,['name'=>'synthetic']);ensure($x['code']===0,'save');return ['saved'=>true];});
    checked('legacy_config_read_four_images','Equipment::plateDetail',function()use($legacy,$seed,$today){$x=$legacy->plateDetail($seed['equipment'],$today);foreach(['morning','noon','night','night_snack']as$i=>$k)ensure($x[$k.'_file_url']==='http://127.0.0.1:18088/synthetic/'.($i+1).'.jpg','image URL mismatch');return ['images'=>4];});
    foreach([1=>'07:30:00',2=>'12:00:00',3=>'18:00:00',4=>'22:00:00']as$meal=>$at){
      checked('legacy_meal_'.$meal,'Plate::dishes + actual Common::mealTimes',function()use($plate,$today,$at,$meal){$GLOBALS['sign_clock']=strtotime($today.' '.$at);$x=$plate->dishes('synthetic-old-plate');ensure($x['img_url']==='http://127.0.0.1:18088/synthetic/'.$meal.'.jpg','meal image mismatch');return ['meal'=>$meal];});
    }
    checked('legacy_missing_date','Plate::dishes',function()use($plate,$today){$GLOBALS['sign_clock']=strtotime($today.' 07:30:00')+86400;$x=$plate->dishes('synthetic-old-plate');ensure($x['img_url']==='','missing day must return empty');return [];});
    $GLOBALS['sign_clock']=strtotime($today.' 07:30:00');
    checked('legacy_missing_device','Plate::dishes',function()use($plate){try{$plate->dishes('synthetic-absent');}catch(\Throwable$e){ensure(strpos($e->getMessage(),'设备不存在')!==false,'wrong failure (possibly SQL)');return ['denied'=>true];}throw new FrontAcceptanceFailure('unknown device accepted');});

    checked('legacy_saved_image_edit_takes_effect','Equipment::plateUpdate/plateDetail + Plate::dishes',function()use($legacy,$plate,$params,$seed,$today){
      $changed=$params;$changed['morning_file_id']=$seed['files'][2];$GLOBALS['sign_clock']=strtotime($today.' 07:30:00');
      try{$r=$legacy->plateUpdate($changed,['name'=>'synthetic']);ensure($r['code']===0,'edit failed');$x=$legacy->plateDetail($seed['equipment'],$today);ensure((int)$x['morning_file_id']===(int)$seed['files'][2],'edited ID not persisted');ensure($plate->dishes('synthetic-old-plate')['img_url']==='http://127.0.0.1:18088/synthetic/2.jpg','edited image not returned');}
      finally{$legacy->plateUpdate($params,['name'=>'synthetic']);}return ['restored_original_fixture'=>true];
    });
    checked('legacy_zero_and_missing_file_return_empty','Equipment::plateUpdate + Plate::dishes',function()use($legacy,$plate,$params){
      ensure((int)\think\Db::name('upload_file')->where('id',2147480000)->count()===0,'missing-ID fixture unexpectedly exists');
      try{foreach([0,2147480000]as$id){$v=$params;$v['morning_file_id']=$id;$legacy->plateUpdate($v,['name'=>'synthetic']);ensure($plate->dishes('synthetic-old-plate')['img_url']==='','zero/missing file leaked image');}}
      finally{$legacy->plateUpdate($params,['name'=>'synthetic']);}return ['cases'=>2];
    });
    checked('legacy_deleted_file_return_empty','Plate::dishes + UploadFile',function()use($plate,$seed){
      $old=\think\Db::name('upload_file')->where('id',$seed['files'][1])->value('is_delete');
      try{\think\Db::name('upload_file')->where('id',$seed['files'][1])->update(['is_delete'=>1]);ensure($plate->dishes('synthetic-old-plate')['img_url']==='','deleted image returned');}
      finally{\think\Db::name('upload_file')->where('id',$seed['files'][1])->update(['is_delete'=>$old]);}return [];
    });
    checked('legacy_wrong_device_type_and_deleted_denied','Plate::plateInfo',function()use($plate,$seed){
      $old=\think\Db::name('equipment')->where('id',$seed['equipment'])->field('type,del_flag')->find();
      try{\think\Db::name('equipment')->where('id',$seed['equipment'])->update(['type'=>7]);legacyRejects(function()use($plate){$plate->dishes('synthetic-old-plate');});\think\Db::name('equipment')->where('id',$seed['equipment'])->update(['type'=>8,'del_flag'=>1]);legacyRejects(function()use($plate){$plate->dishes('synthetic-old-plate');});}
      finally{\think\Db::name('equipment')->where('id',$seed['equipment'])->update($old);}return ['cases'=>2];
    });

    $service=new \app\common\service\ElectronicMenu(10001,[$seed['restaurant']]);
    $input=['display_mode'=>1,'left_restaurant_id'=>$seed['restaurant'],'right_restaurant_id'=>0,'carousel_interval'=>8];$menu=null;
    checked('new_save','ElectronicMenu::save',function()use($service,$input,&$menu){$menu=$service->save($input,'synthetic');ensure($menu['id']>0&&strlen($menu['public_token'])===64,'missing persisted menu');return ['saved'=>true];});
    checked('new_list_scoped','ElectronicMenu::lists',function()use($service,$seed,&$menu){ensure($menu!==null,'menu prerequisite failed');$x=$service->lists(['restaurant_id'=>$seed['restaurant'],'pageSize'=>100]);ensure(in_array((int)$menu['id'],array_map('intval',array_column($x['rows'],'id')),true),'fresh menu missing');ensure($x['total']===count($x['rows']),'Unexpected pagination/count discrepancy in bounded fixture');return ['fresh_menu_present'=>true];});
    foreach([1=>'07:30:00',2=>'12:00:00',3=>'18:00:00',4=>'22:00:00']as$meal=>$at){
      checked('new_play_meal_'.$meal,'ElectronicMenu::play/buildPayload/loadDishes',function()use(&$menu,$today,$at,$meal,$seed){ensure($menu!==null,'menu prerequisite failed');$GLOBALS['sign_clock']=strtotime($today.' '.$at);$x=(new \app\common\service\ElectronicMenu(0))->play($menu['public_token']);ensure($x['context']['meal_times']===$meal,'wrong meal');ensure($x['context']['business_date']===$today,'wrong date');$ds=$x['stalls'][0]['dishes'];ensure(count($ds)===1,'dish count');$a=$ds[0];ensure($a['name']==='Synthetic Dish'&&$a['price']==='13.80','name/price');ensure($a['spicy_level']===2&&$a['health_tags']===['Synthetic tag'],'fallback concealed column or tag issue');ensure((float)$a['carbohydrate']===8.25&&(float)$a['protein']===22.5&&(float)$a['fat']===11.75,'nutrition');ensure($a['image_url']==='http://127.0.0.1:18088/synthetic/dish.jpg','image URL');return ['meal'=>$meal,'dish_count'=>count($ds),'fields_verified'=>8];});
    }
    checked('new_wrong_date_empty','ElectronicMenu::play',function()use(&$menu,$today){$GLOBALS['sign_clock']=strtotime($today.' 07:30:00')+86400;$x=(new \app\common\service\ElectronicMenu(0))->play($menu['public_token']);ensure(count($x['stalls'][0]['dishes'])===0,'wrong day leaked dish');return [];});
    $GLOBALS['sign_clock']=strtotime($today.' 07:30:00');
    checked('new_other_restaurant_denied','ElectronicMenu::preview',function()use($service,$input,$seed){$v=$input;$v['left_restaurant_id']=$seed['restaurant2'];rejects(function()use($service,$v){$service->preview($v);},'other restaurant accepted','没有档口数据权限');return [];});
    checked('new_other_store_denied','ElectronicMenu::detail',function()use(&$menu){rejects(function()use(&$menu){(new \app\common\service\ElectronicMenu(10002))->detail((int)$menu['id']);},'other store accepted','菜牌不存在');return [];});
    checked('new_update_keeps_token_copy_changes_token','ElectronicMenu::save/copy',function()use($service,$input,&$menu){$updated=$service->save(array_merge($input,['id'=>$menu['id'],'carousel_interval'=>12]),'synthetic');ensure($updated['public_token']===$menu['public_token']&&$updated['public_path']===$menu['public_path'],'edit changed public token/path');ensure($updated['carousel_interval']===12&&(int)\think\Db::name('electronic_menu')->where('id',$menu['id'])->value('carousel_interval')===12,'edit did not persist interval');$copy=$service->copy((int)$menu['id'],'synthetic');ensure($copy['public_token']!==$menu['public_token'],'copy reused token');return [];});

    checked('new_wrong_token_and_disabled_or_deleted_menu','ElectronicMenu::play',function()use(&$menu){
      $player=new \app\common\service\ElectronicMenu(0);
      foreach(['not-a-token',str_repeat('f',64)]as$token)rejects(function()use($player,$token){$player->play($token);},'invalid token accepted','菜牌不存在');
      try{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['status'=>0]);rejects(function()use($player,&$menu){$player->play($menu['public_token']);},'disabled menu accepted','菜牌不存在');\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['status'=>1,'is_delete'=>1]);rejects(function()use($player,&$menu){$player->play($menu['public_token']);},'deleted menu accepted','菜牌不存在');}
      finally{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['status'=>1,'is_delete'=>0]);}return ['cases'=>4];
    });
    checked('new_dual_stall_left_clock_and_permission','ElectronicMenu::save/play/lists/detail',function()use($seed,$today){
      $all=new \app\common\service\ElectronicMenu(10001,[$seed['restaurant'],$seed['restaurant2']]);
      $uuid=guid();\think\Db::name('dishes')->insert(['uuid'=>$uuid,'name'=>'Synthetic Right Dish','price'=>'14.25','file_path'=>'synthetic/right.jpg','carbohydrate'=>'9.25','protein'=>'23.50','fat'=>'12.75','status'=>1,'is_del'=>1]);
      $recipeIds=[];foreach([1=>$uuid,2=>$seed['dish_uuid']]as$meal=>$dish)$recipeIds[]=(int)\think\Db::name('recipes')->insertGetId(['uuid'=>guid(),'dishes_uuid'=>$dish,'restaurant_id'=>$seed['restaurant2'],'menu_date'=>$today,'meal_times'=>$meal,'type'=>1,'create_time'=>$today.' 00:00:00','create_by'=>'synthetic']);
      $dual=$all->save(['display_mode'=>2,'left_restaurant_id'=>$seed['restaurant'],'right_restaurant_id'=>$seed['restaurant2'],'carousel_interval'=>8],'synthetic');
      $old=\think\Db::name('restaurants')->where('id',$seed['restaurant2'])->field('morning_start,morning_end,noon_start,noon_end,night_start,night_end')->find();
      try{\think\Db::name('restaurants')->where('id',$seed['restaurant2'])->update(['morning_start'=>'20:00','morning_end'=>'23:59','noon_start'=>'00:00','noon_end'=>'12:00']);$GLOBALS['sign_clock']=strtotime($today.' 07:30:00');$x=(new \app\common\service\ElectronicMenu(0))->play($dual['public_token']);ensure($x['context']['meal_times']===1&&count($x['stalls'])===2,'dual context');ensure((int)$x['stalls'][0]['restaurant_id']===$seed['restaurant']&&(int)$x['stalls'][1]['restaurant_id']===$seed['restaurant2'],'dual restaurants');ensure(count($x['stalls'][1]['dishes'])===1&&$x['stalls'][1]['dishes'][0]['uuid']===$uuid,'right stall must use left-clock breakfast recipe');}
      finally{\think\Db::name('restaurants')->where('id',$seed['restaurant2'])->update($old);}
      $restricted=new \app\common\service\ElectronicMenu(10001,[$seed['restaurant']]);rejects(function()use($restricted,$dual){$restricted->detail((int)$dual['id']);},'dual right permission bypass','没有档口数据权限');$l=$restricted->lists(['pageSize'=>100]);ensure(!in_array((int)$dual['id'],array_map('intval',array_column($l['rows'],'id')),true),'dual leaked through scoped list');$none=(new \app\common\service\ElectronicMenu(10001,[]))->lists([]);ensure($none['total']===0&&$none['rows']===[],'empty permission must show none');\think\Db::name('recipes')->where('id','in',$recipeIds)->delete();\think\Db::name('dishes')->where('uuid',$uuid)->delete();return ['dual_stalls'=>2,'right_uses_left_clock'=>true];
    });
    checked('new_recipe_date_meal_and_dish_status_filters','ElectronicMenu::play/loadDishes',function()use($seed,&$menu,$today){
      $player=new \app\common\service\ElectronicMenu(0);$GLOBALS['sign_clock']=strtotime($today.' 07:30:00');$uuids=[];
      foreach(['previous_day','other_meal','disabled','deleted']as$kind){$uuid=guid();$uuids[]=$uuid;\think\Db::name('dishes')->insert(['uuid'=>$uuid,'name'=>'Synthetic excluded '.$kind,'price'=>'1.00','status'=>$kind==='disabled'?0:1,'is_del'=>$kind==='deleted'?2:1]);\think\Db::name('recipes')->insert(['uuid'=>guid(),'dishes_uuid'=>$uuid,'restaurant_id'=>$seed['restaurant'],'menu_date'=>$kind==='previous_day'?date('Y-m-d',strtotime($today)-86400):$today,'meal_times'=>$kind==='other_meal'?2:1,'type'=>1,'create_time'=>$today.' 00:00:00','create_by'=>'synthetic']);}
      $x=$player->play($menu['public_token']);ensure(array_column($x['stalls'][0]['dishes'],'uuid')===[$seed['dish_uuid']],'wrong date/meal/status dishes leaked');\think\Db::name('recipes')->where('dishes_uuid','in',$uuids)->delete();\think\Db::name('dishes')->where('uuid','in',$uuids)->delete();return ['excluded_cases'=>4];
    });

    $mgr=new \app\common\service\ElectronicMenuDevice(10001,[$seed['restaurant']]);$deviceId='synthetic-tv-'.bin2hex(random_bytes(5));$binding=null;$activated=null;
    checked('device_bind_and_read','ElectronicMenuDevice::createBindCode/activate/config',function()use($mgr,&$menu,&$binding,&$activated,$deviceId){$binding=$mgr->createBindCode((int)$menu['id'],'synthetic');$activated=(new \app\common\service\ElectronicMenuDevice())->activate($binding['bind_code'],$deviceId,'synthetic TV');$x=(new \app\common\service\ElectronicMenuDevice())->config($deviceId,$activated['device_token']);ensure($x['menu_id']===(int)$menu['id']&&strpos($x['public_path'],$menu['public_token'])!==false,'wrong device menu');return ['tokens_not_emitted'=>true];});
    checked('device_code_single_use','ElectronicMenuDevice::activate',function()use(&$binding,$deviceId){rejects(function()use(&$binding,$deviceId){(new \app\common\service\ElectronicMenuDevice())->activate($binding['bind_code'],$deviceId.'b','other');},'code reused','绑定码无效或已过期');return [];});
    checked('device_wrong_token_denied','ElectronicMenuDevice::config',function()use($deviceId){rejects(function()use($deviceId){(new \app\common\service\ElectronicMenuDevice())->config($deviceId,str_repeat('0',64));},'bad token accepted','设备未绑定或绑定已失效');return [];});
    checked('device_logs_roundtrip','ElectronicMenuDevice::collectLogs/logs',function()use($mgr,&$menu,&$activated,$deviceId){$svc=new \app\common\service\ElectronicMenuDevice();$x=$svc->collectLogs($deviceId,$activated['device_token'],[['level'=>'info','event_code'=>'TV_TEST','error_code'=>'TV_OK','message'=>'Synthetic log https://example.invalid/screen/'.$menu['public_token'],'context'=>['http_status'=>502,'secret'=>'must-not-store']]]);ensure($x['accepted']===1,'log rejected');$ds=$mgr->devices((int)$menu['id']);$matched=array_values(array_filter($ds,function($d)use($deviceId){return$d['device_id']===$deviceId;}));ensure(count($matched)===1,'device missing');$logs=$mgr->logs((int)$matched[0]['id']);ensure(count($logs)===1&&$logs[0]['message']==='Synthetic log [url]','log read/sanitization mismatch');$context=json_decode((string)$logs[0]['context_json'],true);ensure(($context['http_status']??null)==='502'&&!isset($context['secret']),'log context allowlist mismatch');ensure(strpos((string)$logs[0]['message'],$menu['public_token'])===false,'log token leaked');return ['sanitized'=>true];});

    checked('device_config_version_refresh_no_token_echo','ElectronicMenuDevice::config',function()use(&$menu,&$activated,$deviceId){
      ensure($activated!==null,'binding prerequisite');$svc=new \app\common\service\ElectronicMenuDevice();$before=$svc->config($deviceId,$activated['device_token']);$old=\think\Db::name('electronic_menu')->where('id',$menu['id'])->value('update_time');
      try{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['update_time'=>'2030-01-01 00:00:00']);$after=$svc->config($deviceId,$activated['device_token']);ensure($before['config_version']!==$after['config_version'],'config version unchanged');ensure($after['refresh_after']===5&&strpos($after['public_path'],'config_version='.$after['config_version'])!==false,'config version URL/refresh mismatch');ensure(!isset($after['device_token'])&&!isset($after['device_token_hash']),'poll response leaked token');}
      finally{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['update_time'=>$old]);}return ['version_changed'=>true,'token_not_echoed'=>true];
    });
    checked('device_wrong_id_and_management_scope_denied','ElectronicMenuDevice::config/devices/logs/unbind',function()use($mgr,&$menu,&$activated,$deviceId){
      ensure($activated!==null,'binding prerequisite');$found=\think\Db::name('electronic_menu_device')->where(['device_id'=>$deviceId,'store_id'=>10001])->find();ensure($found!==null,'device prerequisite');$id=(int)$found['id'];
      rejects(function()use(&$activated,$deviceId){(new \app\common\service\ElectronicMenuDevice())->config($deviceId.'-wrong',$activated['device_token']);},'token accepted for different device','设备未绑定或绑定已失效');
      $foreign=new \app\common\service\ElectronicMenuDevice(10002);rejects(function()use($foreign,&$menu){$foreign->devices((int)$menu['id']);},'cross-store device list','菜牌不存在');rejects(function()use($foreign,$id){$foreign->unbind($id);},'cross-store unbind','电视设备不存在');
      $none=new \app\common\service\ElectronicMenuDevice(10001,[]);rejects(function()use($none,$id){$none->logs($id);},'unauthorized device log read','没有档口数据权限');rejects(function()use($none,$id){$none->unbind($id);},'unauthorized unbind','没有档口数据权限');
      $after=\think\Db::name('electronic_menu_device')->where('id',$id)->find();ensure((int)$after['status']===1&&$after['device_token_hash']===$found['device_token_hash'],'denied call mutated device');return ['cases'=>5,'state_unchanged'=>true];
    });

    checked('device_unbind_invalidates_token','ElectronicMenuDevice::unbind/config',function()use($mgr,&$menu,&$activated,$deviceId){$ds=$mgr->devices((int)$menu['id']);$matched=array_values(array_filter($ds,function($d)use($deviceId){return$d['device_id']===$deviceId;}));$mgr->unbind((int)$matched[0]['id']);rejects(function()use(&$activated,$deviceId){(new \app\common\service\ElectronicMenuDevice())->config($deviceId,$activated['device_token']);},'unbound token accepted','设备未绑定或绑定已失效');return [];});
    checked('device_expired_code_denied','ElectronicMenuDevice::activate',function()use($mgr,&$menu,$deviceId){$code=$mgr->createBindCode((int)$menu['id'],'synthetic');\think\Db::name('electronic_menu_bind_code')->where('code_hash',hash('sha256',$code['bind_code']))->update(['expires_at'=>'2000-01-01 00:00:00']);rejects(function()use($code,$deviceId){(new \app\common\service\ElectronicMenuDevice())->activate($code['bind_code'],$deviceId.'e','expired');},'expired accepted','绑定码无效或已过期');return [];});

    checked('device_rebind_reuses_row_rotates_token','ElectronicMenuDevice::activate',function()use($mgr,$service,&$menu,&$activated,$deviceId){
      ensure($activated!==null,'binding prerequisite');$oldToken=$activated['device_token'];$before=\think\Db::name('electronic_menu_device')->where(['device_id'=>$deviceId,'store_id'=>10001])->find();ensure((int)$before['status']===2,'unbind prerequisite');$copy=$service->copy((int)$menu['id'],'synthetic');$code=$mgr->createBindCode((int)$copy['id'],'synthetic');$activated=(new \app\common\service\ElectronicMenuDevice())->activate($code['bind_code'],$deviceId,'synthetic rebound TV');
      $after=\think\Db::name('electronic_menu_device')->where(['device_id'=>$deviceId,'store_id'=>10001])->select();ensure(count($after)===1&&(int)$after[0]['id']===(int)$before['id'],'rebind created duplicate device');ensure((int)$after[0]['menu_id']===(int)$copy['id']&&$activated['device_token']!==$oldToken,'rebind token/menu unchanged');rejects(function()use($deviceId,$oldToken){(new \app\common\service\ElectronicMenuDevice())->config($deviceId,$oldToken);},'old token valid after rebind','设备未绑定或绑定已失效');$current=(new \app\common\service\ElectronicMenuDevice())->config($deviceId,$activated['device_token']);ensure($current['menu_id']===(int)$copy['id'],'new token points wrong menu');return ['row_reused'=>true,'old_token_rejected'=>true];
    });
    checked('device_bind_code_refresh_and_disabled_menu_rollback','ElectronicMenuDevice::createBindCode/activate',function()use($mgr,&$menu,$deviceId){
      $a=$mgr->createBindCode((int)$menu['id'],'synthetic');$b=$mgr->createBindCode((int)$menu['id'],'synthetic');rejects(function()use($a,$deviceId){(new \app\common\service\ElectronicMenuDevice())->activate($a['bind_code'],$deviceId.'-oldcode','synthetic');},'superseded code accepted','绑定码无效或已过期');
      $before=(int)\think\Db::name('electronic_menu_device')->count();
      try{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['status'=>0]);rejects(function()use($b,$deviceId){(new \app\common\service\ElectronicMenuDevice())->activate($b['bind_code'],$deviceId.'-disabled','synthetic');},'disabled menu activation accepted','大屏配置不存在或已停用');$status=\think\Db::name('electronic_menu_bind_code')->where('code_hash',hash('sha256',$b['bind_code']))->value('status');ensure((int)$status===1,'failed activation did not rollback code claim');ensure((int)\think\Db::name('electronic_menu_device')->count()===$before,'failed activation inserted device');}
      finally{\think\Db::name('electronic_menu')->where('id',$menu['id'])->update(['status'=>1]);}return ['claim_rolled_back'=>true,'device_rows_unchanged'=>true];
    });
    checked('device_disabled_menu_and_device_reject_config','ElectronicMenuDevice::config',function()use(&$activated,$deviceId){
      ensure($activated!==null,'binding prerequisite');$svc=new \app\common\service\ElectronicMenuDevice();$record=\think\Db::name('electronic_menu_device')->where(['device_id'=>$deviceId,'store_id'=>10001])->find();$id=(int)$record['id'];$menuId=(int)$record['menu_id'];
      try{\think\Db::name('electronic_menu')->where('id',$menuId)->update(['status'=>0]);rejects(function()use($svc,&$activated,$deviceId){$svc->config($deviceId,$activated['device_token']);},'config for disabled menu','大屏配置不存在或已停用');\think\Db::name('electronic_menu')->where('id',$menuId)->update(['status'=>1]);\think\Db::name('electronic_menu_device')->where('id',$id)->update(['status'=>2]);rejects(function()use($svc,&$activated,$deviceId){$svc->config($deviceId,$activated['device_token']);},'config for disabled device','设备未绑定或绑定已失效');}
      finally{\think\Db::name('electronic_menu')->where('id',$menuId)->update(['status'=>1]);\think\Db::name('electronic_menu_device')->where('id',$id)->update(['status'=>1]);}return ['cases'=>2];
    });

}catch(\Throwable$e){$report['fatal']=['class'=>get_class($e),'message'=>$e instanceof FrontAcceptanceFailure?$e->getMessage():'Isolated setup/runtime failure; inspect private diagnostic','code'=>$e->getCode()];if(isset($runtime)&&is_string($runtime)&&is_dir($runtime)){$privateMessage=$e->getMessage();if(!empty($cfg['password']))$privateMessage=str_replace($cfg['password'],'[redacted]',$privateMessage);file_put_contents($runtime.'/private-error.txt',$privateMessage);chmod($runtime.'/private-error.txt',0600);}}
$report['passed']=count(array_filter($report['checks'],function($x){return$x['status']==='pass';}));$report['failed']=count(array_filter($report['checks'],function($x){return$x['status']==='fail';}));$report['complete_bounded_service_suite']=!isset($report['fatal'])&&$report['failed']===0;$report['customer_ui_or_authorization_accepted']=false;$report['complete']=!isset($report['fatal'])&&$report['failed']===0;$report['synthetic_only']=true;$report['customer_http_or_device_access']=false;$report['finished']=date(DATE_ATOM);file_put_contents($reportPath,json_encode($report,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));chmod($reportPath,0600);echo json_encode(['passed'=>$report['passed'],'failed'=>$report['failed'],'fatal'=>isset($report['fatal']),'report'=>$reportPath]),"\n";exit($report['complete']?0:1);
}
