<?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 {
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
ini_set('display_errors', '0');
if ($argc !== 3) { fwrite(STDERR, "Usage: php front_php_acceptance.php private-config.json report.json\n"); exit(2); }
$cfg = json_decode(file_get_contents($argv[1]), true);
$reportPath = $argv[2];
$report = ['kind'=>'front-php-real-adapter-v1', '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(($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');
    $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');
    checked('actual_adapter_table_metadata','application/cores/database/Dm.php',function()use($db,$cfg){$ts=$db->getTables();ensure(count($ts)===(int)($cfg['expectedTables']??284),'Unexpected table count');foreach(['ydy_recipes','ydy_dishes','ydy_foods_composition','ydy_restaurants','ydy_user']as$t)ensure(count($db->getFields($t))>0,'getFields empty');return ['tables'=>count($ts)];});
    checked('fixed_user_read','common/service/zywxb/Config.php::fixedUser',function(){$u=\app\common\service\zywxb\Config::fixedUser();ensure((int)$u['id']===2,'Unexpected fixed user');return ['id'=>(int)$u['id'],'type'=>(int)$u['type']];});
    checked('fixed_user_actual_menu_tree','model/User.php::getUserOwnSysMenus',function(){$u=\app\common\service\zywxb\Config::fixedUser();$menu=(new \app\model\User())->getUserOwnSysMenus($u['uuid'],false,false);$items=is_object($menu)?$menu->toArray():$menu;ensure(count($items)>0,'No actual menu authorization');return ['resolved_menu_rows'=>count($items)];});
    checked('fixed_user_restaurant_permission','model/User.php::getUserOwnRestaurantIds + actual user_data row',function(){
      $u=\app\common\service\zywxb\Config::fixedUser();ensure((int)$u['type']===1,'Fixed account must remain ordinary type 1');
      $ids=(new \app\model\User())->getUserOwnRestaurantIds($u['uuid']);
      ensure(is_array($ids),'Unlimited restaurant permission is forbidden');$ids=array_map('strval',$ids);sort($ids,SORT_STRING);ensure($ids===['1'],'Fixed user must have exactly restaurant 1, no other restaurant');
      $restaurant=\think\Db::name('restaurants')->where('id',1)->field('id,uuid,status,is_delete')->find();
      ensure($restaurant && (int)$restaurant['status']===1 && (int)$restaurant['is_delete']===2,'Restaurant 1 must be valid and enabled');
      $grants=\think\Db::name('user_data')->where('user_uuid',$u['uuid'])->where('type',3)->field('data_uuid,staff_uuid_str')->select();
      ensure(count($grants)===1 && $grants[0]['data_uuid']===$restaurant['uuid'],'Grant must reference actual unique restaurant UUID');
      ensure(trim((string)$grants[0]['staff_uuid_str'])==='' || trim((string)$grants[0]['staff_uuid_str'])==='1','Unexpected legacy restaurant grant');
      return ['allowed_restaurant_ids'=>['1'],'account_type'=>1,'grant_uses_real_uuid'=>true,'approval_date'=>'2026-10-24'];
    });
    checked('actual_controller_restaurant_permission_boundary','base/AdminController.php::hasRestaurantPermission',function(){
      $r=new ReflectionClass(\app\base\AdminController::class);$o=$r->newInstanceWithoutConstructor();$m=$r->getMethod('hasRestaurantPermission');$m->setAccessible(true);
      ensure($m->invoke($o,1)===true,'Actual controller denies approved restaurant 1');ensure($m->invoke($o,2)===false,'Actual controller incorrectly allows restaurant 2');return ['restaurant_1'=>true,'restaurant_2'=>false];
    });
    checked('fixed_user_real_null_validity','ydy_user fields + p/logic/Login.php::assertAccountUsable',function(){
      $uuid=\think\Config::get('zywxb.fixed_user_uuid');
      $user=\think\Db::name('user')->where('uuid',$uuid)->field('id,type,status,is_delete,useful_start,useful_end')->find();
      ensure($user && (int)$user['id']===2 && (int)$user['type']===1 && (int)$user['status']===1 && (int)$user['is_delete']===0,'Fixed user state differs from approved scope');
      ensure($user['useful_start']===null && $user['useful_end']===null,'Real database validity must be NULL for both bounds');
      $r=new ReflectionClass(\app\p\logic\Login::class);$o=$r->newInstanceWithoutConstructor();$m=$r->getMethod('assertAccountUsable');$m->setAccessible(true);$m->invoke($o,$user,'isolated-approved-unlimited');
      return ['real_null_bounds'=>true,'login_validity_accepted'=>true,'approval_date'=>'2026-10-24'];
    });
    checked('fixed_user_original_role_menu_membership','ydy_user_role/ydy_role_menu original upgraded source fingerprints',function(){
      $uuid=\think\Config::get('zywxb.fixed_user_uuid');
      $roles=array_map('strval',\think\Db::name('user_role')->where('user_uuid',$uuid)->column('role_uuid'));sort($roles,SORT_STRING);ensure(count($roles)===1,'Original fixed user must retain one role');
      $rows=\think\Db::name('role_menu')->whereIn('role_uuid',$roles)->field('role_uuid,menu_uuid')->select();$pairs=[];foreach($rows as$row)$pairs[]=[(string)$row['role_uuid'],(string)$row['menu_uuid']];
      usort($pairs,function($a,$b){$x=strcmp($a[0],$b[0]);return $x!==0?$x:strcmp($a[1],$b[1]);});
      $expected=['zhct_zywxb'=>'b52e96990ac629a169bc40c9798c69d88d630e88c7d4e66b5a8c553f5f536395','zhct_zywxb2'=>'d0f6d560a8a8a1186aca72e8ae3f114f760c8f16ba0af45932b5440c0ff4c11f'];
      ensure(count($pairs)===18 && hash('sha256',json_encode([$roles,$pairs],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES))===$expected[BUSINESS],'Original role/menu assignments changed');return ['roles'=>1,'role_menu_assignments'=>18,'source_fingerprint_matches'=>true];
    });
    checked('null_useful_period_current_login_policy','p/logic/Login.php::assertAccountUsable',function(){
      $r=new ReflectionClass(\app\p\logic\Login::class);$o=$r->newInstanceWithoutConstructor();$m=$r->getMethod('assertAccountUsable');$m->setAccessible(true);
      $m->invoke($o,['useful_start'=>null,'useful_end'=>null,'status'=>1],'isolated-unlimited');
      $zeroRejected=false;try{$m->invoke($o,['useful_start'=>'0000-00-00 00:00:00','useful_end'=>'0000-00-00 00:00:00','status'=>1],'isolated-zero');}catch(\think\Exception $e){$zeroRejected=true;}
      ensure($zeroRejected,'Zero-date baseline policy differed from reviewed expectation');return ['null_unlimited'=>true,'zero_date_rejected'=>true,'behavior_change_user_approved'=>'2026-10-24'];
    });
    checked('dish_list_pagination','model/Cater.php + BaseModel::inquiry',function(){$x=(new \app\model\Cater())->inquiry(['is_del'=>1,'status'=>1],1,10,['id'=>'asc'],['id','uuid','name','category','weight','price']);ensure(is_array($x)&&count($x)>0,'Dish list empty');return ['rows'=>count($x)];});
    checked('food_composition_list','p/logic/Food.php::lists',function(){$x=(new \app\p\logic\Food())->lists([],1,10,['id'=>'asc'],['id','name','energy','protein','category','minute_sort','reclassify']);ensure(($x['code']??1)===0,'Food list rejected');ensure(count($x['data']['rows'])>0,'Food rows empty');return ['rows'=>count($x['data']['rows'])];});
    $recipe=\think\Db::name('recipes')->where('restaurant_id',1)->order('id','desc')->find();
    if(!$recipe){skipCheck('recipe_reads','No source recipes for restaurant 1');}
    else{
      $menuDate=substr((string)$recipe['menu_date'],0,10);
      checked('recipe_real_raw_join','model/Bill.php::bill_list',function()use($menuDate){$x=(new \app\model\Bill())->bill_list($menuDate,1);ensure(is_array($x),'Recipe join unexpected shape');return ['rows'=>count($x)];});
      checked('recipe_calendar_group','model/Bill.php::timer_shaft',function(){$x=(new \app\model\Bill())->timer_shaft(['restaurant_id'=>1]);ensure(is_array($x)&&count($x)>0,'Calendar empty');return ['dates'=>count($x)];});
      checked('recipe_category_aggregate','model/Bill.php::bill_number',function()use($menuDate){$x=(new \app\model\Bill())->bill_number(['a.menu_date'=>$menuDate,'a.restaurant_id'=>1,'b.is_del'=>1,'b.status'=>1]);ensure(is_array($x),'Recipe aggregate shape');return ['groups'=>count($x)];});
    }
    checked('nutrition_export_real_select','model/Dishes.php::idInquiryAll',function(){$x=(new \app\model\Dishes())->idInquiryAll();ensure(is_array($x)&&count($x)>0,'Nutrition export query empty');return ['rows'=>count($x)];});
    checked('restaurant_pdo_case_normalizer','p/logic/Restaurant.php::normalizeRestaurantResponseRow',function(){$x=(new \app\model\Restaurants())->readById(1);ensure(is_array($x),'Restaurant 1 absent');$r=new ReflectionClass(\app\p\logic\Restaurant::class);$o=$r->newInstanceWithoutConstructor();$m=$r->getMethod('normalizeRestaurantResponseRow');$m->setAccessible(true);$y=$m->invoke($o,$x);foreach(['zpTerm','zpKey','zpApiUrl']as$k)ensure(array_key_exists($k,$y),'Normalized camelCase key absent');return ['legacy_keys_present'=>true];});
    checked('bill_zhipan_direct_model_case_contract','p/logic/Bill.php::zhipanApi + actual Restaurants model; HTTP/sign/log intercepted',function(){
      $model=new \app\model\Restaurants();$row=$model->readById(1);ensure(is_array($row),'Restaurant 1 absent');
      $expected=[];
      foreach(['zpTerm','zpKey','zpApiUrl']as$key){$lookup=array_key_exists($key,$row)?$key:strtolower($key);ensure(array_key_exists($lookup,$row),'Restaurant configuration field absent');$expected[$key]=$row[$lookup];}
      $r=new ReflectionClass(\app\p\logic\Bill::class);$logic=$r->newInstanceWithoutConstructor();$property=$r->getProperty('RestaurantModel');$property->setAccessible(true);$property->setValue($logic,$model);
      $GLOBALS['front_zhipan_boundary']=['signature_calls'=>0,'http_stub_calls'=>0,'log_calls'=>0];
      try{
        $endpoint='/__isolated_contract_no_network__';
        $result=$logic->zhipanApi(['card'=>'isolated-contract-card'],$endpoint,1);
        $captured=$GLOBALS['front_zhipan_boundary'];
        ensure(($result['code']??null)===0,'Real zhipanApi did not report successful boundary stub response');
        ensure($captured['signature_calls']===1 && $captured['http_stub_calls']===1,'Unexpected signature/HTTP boundary call count');
        ensure(array_key_exists('term',$captured['signature_params']) && $captured['signature_params']['term']===$expected['zpTerm'],'Signature term differs from actual restaurant row');
        ensure($captured['signature_key']===$expected['zpKey'],'Signature key differs from actual restaurant row');
        ensure(array_key_exists('term',$captured['http_params']) && $captured['http_params']['term']===$expected['zpTerm'],'HTTP term differs from actual restaurant row');
        ensure(($captured['http_params']['sig']??null)==='isolated-front-contract-signature','Signature result not passed to boundary');
        ensure($captured['http_url']===$expected['zpApiUrl'].$endpoint,'HTTP URL differs from actual restaurant row');
        return ['actual_model_read'=>true,'actual_logic_called'=>true,'term_key_url_match'=>true,'http_stub_calls'=>1,'external_network_requests'=>0,'sensitive_values_emitted'=>false];
      }finally{unset($GLOBALS['front_zhipan_boundary']);}
    });
    foreach(['dishes_cook_method']as$method)checked('restaurant_analysis_'.$method,'model/Restaurant.php::'.$method,function()use($method){$x=(new \app\model\Restaurant())->$method();ensure(is_array($x),'Analysis shape unexpected');return ['groups'=>count($x)];});
    checked('future_pal_float31_boundary_expressions','common/service/MetabolismPalFloat31.php + source-backed test oracles',function()use($pdo){
      $cases=['1.25'=>1.2,'1.35'=>1.4,'0.05'=>0.0,'99.95'=>99.9,'-1.25'=>-1.2,'1.15'=>1.1,'2.15'=>2.1,'3.15'=>3.1,'2.45'=>2.5];$n=0;
      foreach($cases as$input=>$expected){$input=(string)$input;$expression=\app\common\service\MetabolismPalFloat31::writeValue($input,true);ensure($expression instanceof \think\db\Expression,'PAL mapped expression missing');$sql=(string)$expression;ensure(preg_match('/^CAST\([-+0-9.eE]+ AS REAL\)$/D',$sql)===1,'Unsafe PAL expression');$got=$pdo->query('SELECT '.$sql.' FROM DUAL')->fetchColumn();ensure(bin2hex(pack('g',(float)$got))===bin2hex(pack('g',$expected)),'PAL REAL expression differs from MySQL source oracle');ensure(\app\common\service\MetabolismPalFloat31::writeValue($input,false)===$input,'MySQL PAL path changed');$n++;}
      return ['cases'=>$n,'mode'=>'helper plus real DM SELECT only','dml'=>0,'full_application_write_path_verified'=>false];
    });
    checked('future_date_menu_float32_boundary_expressions','common/service/DateMenuFloat32.php + DateMenuFloat32WriteTest',function()use($pdo){
      $helper=\app\common\service\DateMenuFloat32::class;
      foreach(['0.1',1,1.000000059604645]as$input){ensure($helper::forWrite($input,false)===$input,'MySQL date-menu scalar changed');$expression=$helper::forWrite($input,true);ensure($expression instanceof \think\db\Expression,'Date-menu mapped expression missing');ensure(preg_match('/^CAST\([-+0-9.eE]+ AS REAL\)$/D',(string)$expression)===1,'Unsafe date-menu expression');$got=$pdo->query('SELECT '.(string)$expression.' FROM DUAL')->fetchColumn();$expected=is_float($input)?'3f800001':bin2hex(pack('G',(float)$input));ensure(bin2hex(pack('G',(float)$got))===$expected,'Date-menu float32 boundary mismatch');}
      return ['cases'=>3,'dml'=>0,'full_application_write_path_verified'=>false];
    });
    if(($cfg['allowRollbackWrite']??false)!==true){skipCheck('recipe_update_rollback','allowRollbackWrite not explicitly enabled');}
    elseif(!$recipe){skipCheck('recipe_update_rollback','No existing recipe fixture');}
    else checked('recipe_update_rollback','model/BaseModel.php::modifyById + DM transaction',function()use($recipe,$db){
      $id=$recipe['id'];$model=new \app\model\Bill();$before=$model->readById($id);$original=(string)$before['limit_num'];$value=$original==='7'?8:7;$begun=false;
      try{$db->startTrans();$begun=true;$affected=$model->modifyById($id,['limit_num'=>$value]);ensure((int)$affected===1,'Update affected count mismatch');$during=$model->readById($id);ensure((int)$during['limit_num']===$value,'Read-after-write mismatch');$db->rollback();$begun=false;}finally{if($begun)$db->rollback();}
      $after=$model->readById($id);ensure($before===$after,'Rollback changed source row');return ['updated_rows'=>1,'rolled_back'=>true,'all_row_fields_unchanged'=>true];
    });
    skipCheck('price_comparison_hcgj','Customer independent hcgj DB not supplied; no fabricated target');
    skipCheck('external_devices_printing_http','No external HTTP/queue consumers in isolated acceptance');
    skipCheck('browser_excel_render_and_upload','This runner validates SQL/model behavior, not rendered web UI or uploaded spreadsheet workflow');
}catch(Throwable $e){$report['fatal']=['class'=>get_class($e),'code'=>(string)$e->getCode(),'message'=>$e instanceof FrontAcceptanceFailure?substr($e->getMessage(),0,180):'Bootstrap/fingerprint/SQL failed; inspect isolated task locally'];}
$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['skipped']=count(array_filter($report['checks'],function($x){return$x['status']==='skip';}));$report['accepted']=!isset($report['fatal'])&&$report['failed']===0&&$report['skipped']===0;
$report['finished']=date(DATE_ATOM);file_put_contents($reportPath,json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));chmod($reportPath,0600);echo json_encode(['passed'=>$report['passed'],'failed'=>$report['failed'],'skipped'=>$report['skipped'],'fatal'=>isset($report['fatal']),'report'=>$reportPath],JSON_UNESCAPED_SLASHES),"\n";exit(isset($report['fatal'])||$report['failed']?1:0);

}
