<?php
/** Isolated front migration acceptance. Does NOT bootstrap web routes, queues or Redis. */
declare(strict_types=1);
namespace app\p\logic {
 function date($format,$timestamp=null){return \date($format,$timestamp===null?($GLOBALS["legacy_clock"]??time()):$timestamp);}
    // 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\m\logic {function date($format,$timestamp=null){return \date($format,$timestamp===null?($GLOBALS["legacy_clock"]??time()):$timestamp);}}
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'=>'legacy-console-real-dm-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');
    ensure(in_array($schema,['FRONT24_APP3','FRONT24_APP4'],true),'Final isolated read-only 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');

    $package=dirname(dirname(realpath($argv[1])));$baseline=json_decode(file_get_contents($package.'/verification/source/baseline.json'),true);
    $columns=array_column($baseline['metadata']['ydy_dishes'],'name');$idIndex=array_search('id',$columns,true);$nameIndex=array_search('name',$columns,true);
    $gz=gzopen($package.'/verification/source/ydy_dishes.hex.jsonl.gz','r');$sample=json_decode(gzgets($gz),true);gzclose($gz);
    $oracleId=(int)hex2bin($sample[$idIndex]);$oracleHex=strtoupper($sample[$nameIndex]);
    $actualName=\think\Db::name('dishes')->where('id',$oracleId)->value('name');
    ensure(preg_match('//u',$actualName)===1&&strtoupper(bin2hex($actualName))===$oracleHex,'PHP client encoding differs from UTF8 source baseline');
    $report['source_chinese_utf8_verified']=true;
    function assertJsonReady($value):void{ensure(json_encode($value,JSON_UNESCAPED_UNICODE)!==false,'Response is not valid UTF8 JSON');}
    \think\Config::set('meal_time_set',['morning'=>['00:00:00','08:59:59'],'noon'=>['09:00:00','14:59:59'],'night'=>['15:00:00','19:29:59','19:30:00','23:59:59'],'extra'=>['00:00:00','23:59:59']]);
    $restaurant=(new \app\model\Restaurants())->readById(1);
    ensure(is_array($restaurant),'restaurant absent');
    $groups=\think\Db::query('SELECT menu_date,meal_times,COUNT(*) AS n FROM ydy_recipes WHERE restaurant_id=1 AND meal_times IN (1,2,3) GROUP BY menu_date,meal_times ORDER BY n DESC');
    $samples=[];foreach($groups as$row){$meal=(int)$row['meal_times'];if(!isset($samples[$meal]))$samples[$meal]=$row;}
    ensure(count($samples)===3,'Need three nonempty historical meals, cannot pass partial reads');
    $report['expected_checks']=13;
    $report['clock_control']='historical nonempty source meal dates; no data writes';
    $report['database_writes']=false;$report['actual_browser_or_device']=false;
    foreach($samples as$meal=>$row){
      $day=substr((string)$row['menu_date'],0,10);$prefix=[1=>'morning',2=>'noon',3=>'night'][$meal];
      $a=strtotime($day.' '.$restaurant[$prefix.'_start']);$b=strtotime($day.' '.$restaurant[$prefix.'_end']);
      $GLOBALS['legacy_clock']=(int)(($a+$b)/2);
      $logic=new \app\m\logic\ControlScreen();
      $actual=(new \app\p\logic\Api())->getMealTimes('',1);ensure($actual===$meal,'Cannot choose matching real restaurant meal time');
      checked('legacy_supply_'.$meal,'ControlScreen::everyMealSupply',function()use($logic,$meal){$x=$logic->everyMealSupply(1);assertJsonReady($x);ensure(is_array($x)&&$x['meal_times']===$meal,'response meal');ensure(isset($x['cookbook'],$x['ingredient'],$x['cook_methods']),'missing legacy shape');$n=array_sum(array_column($x['cookbook'],'num'));ensure($n>0,'empty supply must not count as tested');return ['meal'=>$meal,'dish_category_total'=>$n,'ingredient_groups'=>count($x['ingredient'])];});
      checked('legacy_cuisine_'.$meal,'ControlScreen::cuisineKind',function()use($logic,$meal,$row,$day){$x=$logic->cuisineKind(1);assertJsonReady($x);ensure(count($x)===6,'six food categories required');$n=array_sum(array_column($x,1));ensure($n>0&&$n<=(int)$row['n'],'invalid count');$oracle=\think\Db::query('SELECT type,COUNT(*) AS n FROM ydy_recipes WHERE restaurant_id=1 AND menu_date=? AND meal_times=? GROUP BY type',[$day,$meal]);$expected=array_fill(1,6,0);foreach($oracle as$v)if(isset($expected[(int)$v['type']]))$expected[(int)$v['type']]=(int)$v['n'];foreach($expected as$k=>$v)ensure((int)$x[$k][1]===$v,'category count differs from independent SQL');return ['meal'=>$meal,'categories'=>count($x),'rows'=>$n];});
      checked('legacy_ingredient_'.$meal,'ControlScreen::offerIngredient',function()use($logic,$meal,$day){$x=$logic->offerIngredient(1);assertJsonReady($x);ensure(is_array($x)&&count($x)>0&&array_sum($x)>0,'missing or empty ingredients');$oracle=\think\Db::query('SELECT category,COUNT(*) AS n FROM ydy_dish WHERE is_delete=0 AND dishes_uuid IN (SELECT dishes_uuid FROM ydy_recipes WHERE restaurant_id=1 AND menu_date=? AND meal_times=?) GROUP BY category',[$day,$meal]);$counts=[];foreach($oracle as$v)$counts[(int)$v['category']]=(int)$v['n'];foreach(['谷薯'=>[1,2],'畜禽水产'=>[3,4,5],'蔬菜'=>[6],'水果'=>[8],'蛋奶大豆'=>[10,11,12],'坚果'=>[9]]as$k=>$ids){$v=0;foreach($ids as$id)$v+=$counts[$id]??0;ensure((int)$x[$k]===$v,'ingredient differs from independent SQL');}return ['meal'=>$meal,'ingredient_groups'=>count($x),'count_sum'=>array_sum($x)];});
      checked('legacy_recommend_v1_'.$meal,'ControlScreen::recommend_v1',function()use($logic,$meal){$x=$logic->recommend_v1(1);assertJsonReady($x);ensure(is_array($x)&&count($x)>0,'invalid or empty recommendation shape');foreach($x as$v){ensure(!empty($v['dishes']['name'])&&is_string($v['dishes']['file_path'])&&array_key_exists('message',$v),'recommendation field contract');ensure(!empty($v['section']['nutrient'])&&count($v['section']['energy_ratio'])===3,'nutrition section contract');foreach($v['section']['nutrient']as$a)ensure(is_numeric($a['content'])&&is_finite((float)$a['content']),'nonfinite nutrition');foreach($v['section']['energy_ratio']as$a)ensure(is_numeric($a['ratio'])&&(float)$a['ratio']>=0&&(float)$a['ratio']<=100,'ratio out of range');}return ['meal'=>$meal,'top_level_entries'=>count($x),'nonempty'=>count($x)>0];});
    }

    checked('legacy_template2_zip','Bill::exportDishesPai + DishesPaiTemplateServer + GD/ZipArchive',function()use($samples,$runtime){
      $row=reset($samples);$day=substr((string)$row['menu_date'],0,10);$meal=(int)$row['meal_times'];
      $expected=\think\Db::query('SELECT COUNT(*) AS n FROM ydy_recipes r JOIN ydy_dishes d ON r.dishes_uuid=d.uuid WHERE r.restaurant_id=1 AND r.menu_date=? AND r.meal_times=?',[$day,$meal]);
      $n=(int)$expected[0]['n'];ensure($n>0,'empty export');
      ob_start();try{(new \app\p\logic\Bill())->exportDishesPai($day,$meal,1);$bytes=ob_get_contents();}finally{ob_end_clean();}
      ensure(substr($bytes,0,2)==='PK','not ZIP');$file=$runtime.'/legacy-template2.zip';file_put_contents($file,$bytes);chmod($file,0600);
      $zip=new \ZipArchive();ensure($zip->open($file)===true,'ZIP unreadable');ensure($zip->numFiles===$n,'export count mismatch');
      $dimensions=[];for($i=0;$i<$zip->numFiles;$i++){$png=$zip->getFromIndex($i);ensure(substr($png,0,8)==="\x89PNG\r\n\x1a\n",'invalid PNG signature');$size=getimagesizefromstring($png);ensure($size!==false&&$size[0]>0&&$size[1]>0,'bad PNG image');$dimensions[$size[0].'x'.$size[1]]=true;if($i===0){file_put_contents($runtime.'/legacy-template2-preview.png',$png);chmod($runtime.'/legacy-template2-preview.png',0600);}}
      $zip->close();return ['images'=>$n,'dimensions'=>array_keys($dimensions),'zip_bytes'=>strlen($bytes),'private_artifact_retained'=>true,'browser_download_not_tested'=>true];
    });

    $report['framework_query_count']=\think\Db::$queryTimes;
    $report['framework_execute_count']=\think\Db::$executeTimes;
    ensure(\think\Db::$executeTimes===0,'Read-only test unexpectedly issued execute');
}catch(\Throwable$e){$report['fatal']=['class'=>get_class($e),'code'=>$e->getCode(),'message'=>$e instanceof FrontAcceptanceFailure?$e->getMessage():'Isolated runtime/SQL failure'];if(isset($runtime)&&is_dir($runtime)){file_put_contents($runtime.'/legacy-console-private-error.txt',str_replace($cfg['password']??'__none__','[redacted]',$e->getMessage()));chmod($runtime.'/legacy-console-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']=!isset($report['fatal'])&&$report['failed']===0;$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);
}
