<?php

declare(strict_types=1);

// One lane per process: A or B, never both. Bill business tables are SELECT-only;
// the only DML is this run's type1 permission fixture.
if ($argc!==2 || !in_array($argv[1],
    ['--run-approved-a-bill4','--run-approved-b-bill4'],true)
    || getenv('CDX_APPROVED_AB_BILL_FIXTURE')!=='1') {
    fwrite(STDERR,"Explicit reviewed A/B fixture gate required\n");
    exit(2);
}

$lane=$argv[1]==='--run-approved-a-bill4'?'A':'B';
$runtime=$lane==='A'
    ? ['root'=>'/tmp/zhct-http-store-a.l5omH8','pid'=>21023,'port'=>18097]
    : ['root'=>'/tmp/zhct-http-store-mysql.dQTTBP','pid'=>10001,'port'=>18081];
$project='/workspace/zhct/zhctproject/store';
$expectedSources=[
    'model'=>'daa8f7181928d06b53e57504f229eaf04088e3ea7eaccfad55c07d091db03c18',
    'controller'=>'8d99ea508e165d694a156d43420cb98f3a809a0fb9a120716e4f9dd90a4b9cf2',
    'bill_logic'=>'30850e03938d2860eb0230a5916860a5282c901c2ad0033455312337b2aad5ee',
    'recipe_batch'=>'0313e936e55b97d13e97a2437ba755409b4341cc670ac83bf9282cb5658f6db4',
];
$marker='CDXPARBILL'.strtoupper(bin2hex(random_bytes(4)));
$makeUuid=static function(): string {
    $hex=bin2hex(random_bytes(16));
    return substr($hex,0,8).'-'.substr($hex,8,4).'-'.substr($hex,12,4)
        .'-'.substr($hex,16,4).'-'.substr($hex,20,12);
};
$userUuid=$makeUuid();
$roleUuid=$makeUuid();
$menuUuid='';
$restaurantUuid='';
$fixtureTouched=false;
$transportUncertain=false;
$phase='runtime-boundary';
$result=['lane'=>$lane,'cases'=>[],'manifest'=>[
    'marker'=>$marker,'user_uuid'=>$userUuid,'role_uuid'=>$roleUuid,
    'only_synthetic_identifiers'=>true,
]];
$open=null;
$scalar=null;

try {
    $cmd=str_replace("\0",' ',(string)file_get_contents(
        '/proc/'.$runtime['pid'].'/cmdline'));
    if (strpos($cmd,' -S 127.0.0.1:'.$runtime['port'].' ')===false
        || strpos($cmd,$runtime['root'].'/public')===false
        || substr_count($cmd,' -S ')!==1) {
        throw new RuntimeException('Wrong one-worker isolated PHP runtime');
    }
    $workers='';
    foreach (explode("\0",(string)file_get_contents(
        '/proc/'.$runtime['pid'].'/environ')) as $entry) {
        $parts=explode('=',$entry,2);
        if (count($parts)===2 && $parts[0]==='PHP_CLI_SERVER_WORKERS') {
            $workers=$parts[1];
        }
    }
    if (!in_array($workers,['','1'],true)) {
        throw new RuntimeException('Multiple PHP HTTP workers not approved');
    }
    $boundaryContext=stream_context_create(['http'=>[
        'timeout'=>3,'ignore_errors'=>true,
    ]]);
    $boundaryRaw=@file_get_contents('http://127.0.0.1:'
        .$runtime['port'].'/__poc_boundary',false,$boundaryContext);
    $boundaryHeaders=$http_response_header??[];
    $boundaryStatus=preg_match('/^HTTP\/\S+\s+(\d+)/',
        (string)($boundaryHeaders[0]??''),$boundaryMatch)
        ? (int)$boundaryMatch[1]:0;
    $boundary=json_decode((string)$boundaryRaw,true);
    if ($boundaryStatus!==200 || !is_array($boundary)
        || empty($boundary['external_stubs'])
        || empty($boundary['redis_isolated']) || !empty($boundary['real_queue'])) {
        throw new RuntimeException('A/B isolated HTTP boundary failed');
    }
    $hashes=[
        'model'=>$runtime['root'].'/application/model/Bill.php',
        'controller'=>$runtime['root'].'/application/p/controller/Bill.php',
        'bill_logic'=>$runtime['root'].'/application/p/logic/Bill.php',
        'recipe_batch'=>$runtime['root'].'/application/p/logic/RecipeBatch.php',
    ];
    foreach ($hashes as $key=>$path) {
        if (!hash_equals($expectedSources[$key],
            (string)hash_file('sha256',$path))) {
            throw new RuntimeException('A/B source hash gate failed');
        }
    }
    $phase='code-local-development-mysql';
    $load=static function(string $path): array { return require $path; };
    $local=$load($project.'/application/config/local/database.php');
    $selected=$load($runtime['root'].'/application/database.php');
    foreach (['hostname','database','username','password'] as $key) {
        if (!isset($selected[$key],$local[$key])
            || $selected[$key]!==$local[$key]) {
            throw new RuntimeException('Runtime differs from code-local DB');
        }
    }
    if (($selected['type']??null)!=='mysql'
        || (int)($selected['hostport']??3306)
            !==(int)($local['hostport']??3306)) {
        throw new RuntimeException('Wrong A/B dialect or port');
    }
    $open=static function() use ($local): PDO {
        return new PDO('mysql:host='.$local['hostname'].';port='.
            (int)($local['hostport']??3306).';dbname='.$local['database']
            .';charset=utf8mb4',(string)$local['username'],
            (string)$local['password'],[
                PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_EMULATE_PREPARES=>false,
                PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,
                PDO::ATTR_TIMEOUT=>8,
            ]);
    };
    $db=$open();
    if (strpos((string)$db->query('SELECT VERSION()')->fetchColumn(),'5.6.')===false) {
        throw new RuntimeException('Not development MySQL5.6');
    }
    $schema=(string)$local['database'];
    $scalar=static function(PDO $conn,string $sql,array $args=[]): int {
        $stmt=$conn->prepare($sql);
        $stmt->execute($args);
        return (int)$stmt->fetchColumn();
    };

    $phase='bill-business-columns';
    $businessRequired=[
        'ydy_recipes'=>['id','uuid','dishes_uuid','menu_date',
            'restaurant_id','meal_times','limit_num'],
        'ydy_dishes'=>['uuid','name','category','file_path','price',
            'is_del','status','store_id'],
        'ydy_date_menu'=>['dishes_uuid','menu_date','restaurant_id',
            'meal_times','meal_type'],
        'ydy_restaurants'=>['id','uuid','status','is_delete'],
        'ydy_menu'=>['uuid','filter'],
    ];
    $businessColumns=$db->prepare('SELECT COLUMN_NAME FROM information_schema.COLUMNS'
        .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?');
    foreach ($businessRequired as $table=>$columns) {
        $businessColumns->execute([$schema,$table]);
        $actual=array_map('strtolower',$businessColumns->fetchAll(PDO::FETCH_COLUMN));
        if (array_diff($columns,$actual)) {
            throw new RuntimeException('Bill business table column gate failed');
        }
    }

    $phase='permission-schema';
    $required=[
        'ydy_user'=>['id','uuid','account','pwd','salt','name','type',
            'status','create_time','create_by','is_delete'],
        'ydy_role'=>['id','uuid','name','status','create_time','create_by'],
        'ydy_user_role'=>['id','user_uuid','role_uuid','create_time','create_by'],
        'ydy_role_menu'=>['id','role_uuid','menu_uuid','create_time','create_by'],
        'ydy_user_data'=>['id','user_uuid','data_uuid','type'],
    ];
    $inserted=[
        'ydy_user'=>['uuid','account','pwd','salt','name','type',
            'status','create_time','create_by','is_delete'],
        'ydy_role'=>['uuid','name','status','create_time','create_by'],
        'ydy_user_role'=>['user_uuid','role_uuid','create_time','create_by'],
        'ydy_role_menu'=>['role_uuid','menu_uuid','create_time','create_by'],
        'ydy_user_data'=>['user_uuid','data_uuid','type'],
    ];
    $columnsStmt=$db->prepare('SELECT COLUMN_NAME,IS_NULLABLE,COLUMN_DEFAULT'
        .' FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=?');
    foreach ($required as $table=>$names) {
        $columnsStmt->execute([$schema,$table]);
        $columns=[];
        foreach ($columnsStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $columns[strtolower((string)$row['COLUMN_NAME'])]=$row;
        }
        if (array_diff($names,array_keys($columns))) {
            throw new RuntimeException('Permission table columns missing');
        }
        foreach ($columns as $name=>$column) {
            if ($name!=='id' && $column['IS_NULLABLE']==='NO'
                && $column['COLUMN_DEFAULT']===null
                && !in_array($name,$inserted[$table],true)) {
                throw new RuntimeException('Permission insert omits required field');
            }
        }
        if ($scalar($db,'SELECT COUNT(*) FROM information_schema.TRIGGERS'
            .' WHERE TRIGGER_SCHEMA=? AND EVENT_OBJECT_TABLE=?',
            [$schema,$table])!==0
            || $scalar($db,'SELECT COUNT(*)'
                .' FROM information_schema.KEY_COLUMN_USAGE'
                .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?'
                .' AND REFERENCED_TABLE_NAME IS NOT NULL',
                [$schema,$table])!==0) {
            throw new RuntimeException('Permission trigger/FK gate failed');
        }
    }
    $requiredUnique=[
        'ydy_role'=>['name'],
        'ydy_user_role'=>['user_uuid','role_uuid'],
        'ydy_role_menu'=>['role_uuid','menu_uuid'],
        'ydy_user_data'=>['user_uuid','data_uuid','type'],
    ];
    $indexStmt=$db->prepare('SELECT INDEX_NAME,NON_UNIQUE,SEQ_IN_INDEX,'
        .' COLUMN_NAME FROM information_schema.STATISTICS'
        .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?'
        .' ORDER BY INDEX_NAME,SEQ_IN_INDEX');
    foreach ($requiredUnique as $table=>$names) {
        $indexStmt->execute([$schema,$table]);
        $indexes=[];
        foreach ($indexStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $key=(string)$row['INDEX_NAME'];
            $indexes[$key]['unique']=(int)$row['NON_UNIQUE']===0;
            $indexes[$key]['columns'][]=strtolower((string)$row['COLUMN_NAME']);
        }
        $found=false;
        foreach ($indexes as $index) {
            if ($index['unique'] && $index['columns']===$names) $found=true;
        }
        if (!$found) throw new RuntimeException('Permission unique key gate failed');
    }
    foreach (['ydy_user'=>'account','ydy_role'=>'name',
        'ydy_user_role'=>'create_by','ydy_role_menu'=>'create_by'] as $table=>$field) {
        if ($scalar($db,'SELECT COUNT(*) FROM '.$table.' WHERE '.$field
            .' LIKE ?', ['CDXPARBILL%'])!==0) {
            throw new RuntimeException('Prior recipe fixture marker residue');
        }
    }

    $phase='natural-relation';
    $candidateSql='SELECT r.menu_date menu_date,'
        .' r.restaurant_id restaurant_id,r.meal_times meal_times,'
        .' r.dishes_uuid dishes_uuid,st.uuid restaurant_uuid,'
        .' d.category category FROM ydy_recipes r'
        .' JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
        .' JOIN ydy_restaurants st ON st.id=r.restaurant_id'
        .' WHERE d.is_del=1 AND d.status=1 AND d.store_id=10001'
        .' AND d.category BETWEEN 1 AND 6'
        .' AND st.status=1 AND st.is_delete=2'
        .' AND r.meal_times BETWEEN 1 AND 3 AND EXISTS('
        .' SELECT 1 FROM ydy_date_menu dm'
        .' WHERE dm.dishes_uuid=r.dishes_uuid'
        .' AND dm.menu_date=r.menu_date'
        .' AND dm.restaurant_id=r.restaurant_id'
        .' AND dm.meal_times=r.meal_times AND dm.meal_type=3)'
        .' ORDER BY r.menu_date DESC LIMIT 5000';
    $targetRelationSha12='5f07d785b0f1';
    $matched=[];
    foreach ($db->query($candidateSql)->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $day=substr((string)$row['menu_date'],0,10);
        if (!preg_match('/^\\d{4}-\\d{2}-\\d{2}$/',$day)) continue;
        $fingerprint=substr(hash('sha256',(string)$row['dishes_uuid'].'|'
            .$day.'|'.(int)$row['restaurant_id'].'|'
            .(int)$row['meal_times']),0,12);
        if ($fingerprint===$targetRelationSha12) $matched[$fingerprint]=$row;
    }
    if (count($matched)!==1) {
        throw new RuntimeException('Approved A/B natural Bill relation unavailable');
    }
    $candidate=reset($matched);
    $date=substr((string)$candidate['menu_date'],0,10);
    $restaurantId=(int)$candidate['restaurant_id'];
    $mealTimes=(int)$candidate['meal_times'];
    $restaurantUuid=(string)$candidate['restaurant_uuid'];
    if ($mealTimes<1 || $mealTimes>3) {
        throw new RuntimeException('Only PC meals 1-3 approved');
    }
    $sourceRecipeCount=$scalar($db,'SELECT COUNT(*) FROM ydy_recipes'
        .' WHERE menu_date=? AND restaurant_id=? AND meal_times=?',
        [$date,$restaurantId,$mealTimes]);
    $sourceVisibleCount=$scalar($db,'SELECT COUNT(*) FROM ydy_recipes r'
        .' JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
        .' WHERE r.menu_date=? AND r.restaurant_id=?'
        .' AND r.meal_times=? AND d.is_del=1',
        [$date,$restaurantId,$mealTimes]);
    $sourceCopyableCount=$scalar($db,'SELECT COUNT(*) FROM ydy_recipes r'
        .' JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
        .' WHERE r.menu_date=? AND r.restaurant_id=?'
        .' AND r.meal_times=? AND d.store_id=10001'
        .' AND d.is_del=1 AND d.status=1',
        [$date,$restaurantId,$mealTimes]);
    $sourceDistinctDates=$scalar($db,'SELECT COUNT(DISTINCT menu_date)'
        .' FROM ydy_recipes WHERE restaurant_id=?',[$restaurantId]);
    if ($sourceRecipeCount<1 || $sourceVisibleCount<1
        || $sourceCopyableCount<1 || $sourceDistinctDates<1) {
        throw new RuntimeException('Bill source nonempty gate failed');
    }
    $result['candidate']=[
        'relation_sha12'=>$targetRelationSha12,
        'menu_date_sha12'=>substr(hash('sha256',$date),0,12),
        'meal_times'=>$mealTimes,'recipe_count'=>$sourceRecipeCount,
        'visible_bill_count'=>$sourceVisibleCount,
        'copyable_count'=>$sourceCopyableCount,
        'distinct_dates'=>$sourceDistinctDates,
    ];
    $phase='exact-bill-menu';
    $menuStmt=$db->prepare('SELECT uuid,filter FROM ydy_menu'
        .' WHERE LOWER(filter) LIKE ?');
    $menuStmt->execute(['%p/bill/look_bill_list%']);
    $matches=[];
    foreach ($menuStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $filter='/'.trim(strtolower((string)$row['filter']),' /');
        if ($filter==='/p/bill/look_bill_list') {
            $matches[]=(string)$row['uuid'];
        }
    }
    if (count($matches)!==1) {
        throw new RuntimeException('Bill menu absent or ambiguous');
    }
    $menuUuid=$matches[0];
    if ($scalar($db,'SELECT COUNT(*) FROM ydy_user WHERE uuid=?',[$userUuid])!==0
        || $scalar($db,'SELECT COUNT(*) FROM ydy_role WHERE uuid=?',[$roleUuid])!==0
        || $scalar($db,'SELECT COUNT(*) FROM ydy_user_data'
            .' WHERE user_uuid=?',[$userUuid])!==0) {
        throw new RuntimeException('Synthetic UUID collision');
    }

    // Isolate fixture writes from the potentially long-lived read-only gate
    // connection. This does not change application/runtime DB parameters.
    $phase='fixture-fresh-connection';
    $refreshAt=microtime(true);
    $db=$open();
    $health=$db->prepare('SELECT 1');
    $health->execute();
    if ((int)$health->fetchColumn()!==1) {
        throw new RuntimeException('Fresh fixture PDO SELECT 1 failed');
    }
    $result['fixture_fresh_connection']=[
        'select1_passed'=>true,
        'elapsed_ms'=>(int)round((microtime(true)-$refreshAt)*1000),
    ];

    $phase='private-manifest';
    $manifestDir='/tmp/zhct-store-ab-bill4-20261002';
    if (!is_dir($manifestDir)) {
        if (!mkdir($manifestDir,0700)) {
            throw new RuntimeException('Private manifest directory create failed');
        }
    }
    if (is_link($manifestDir) || (fileperms($manifestDir)&0777)!==0700) {
        throw new RuntimeException('Private manifest directory boundary failed');
    }
    $manifestPath=$manifestDir.'/'.$lane.'-'.$marker.'.json';
    $manifest=[
        'lane'=>$lane,'marker'=>$marker,'user_uuid'=>$userUuid,
        'role_uuid'=>$roleUuid,'menu_uuid'=>$menuUuid,
        'restaurant_uuid'=>$restaurantUuid,
        'business_tables_written'=>false,'permission_fixture_phase'=>'planned',
    ];
    $manifestFile=@fopen($manifestPath,'x');
    if (!$manifestFile) throw new RuntimeException('Private manifest file exists');
    chmod($manifestPath,0600);
    $manifestJson=json_encode($manifest,JSON_UNESCAPED_SLASHES);
    if (!is_string($manifestJson)
        || fwrite($manifestFile,$manifestJson)!==strlen($manifestJson)) {
        fclose($manifestFile);
        throw new RuntimeException('Manifest write incomplete');
    }
    fclose($manifestFile);
    $result['manifest']['private_path']=$manifestPath;

    $phase='fixture-user';
    $now=date('Y-m-d H:i:s');
    $fixtureTouched=true;
    $db->prepare('INSERT INTO ydy_user'
        .' (uuid,account,pwd,salt,name,type,status,create_time,create_by,is_delete)'
        .' VALUES (?,?,?,?,?,1,1,?,?,0)')
        ->execute([$userUuid,$marker,'DISABLED',
            strtoupper(bin2hex(random_bytes(8))),$marker,$now,$marker]);
    $actorStmt=$open()->prepare('SELECT id,type,status,name'
        .' FROM ydy_user WHERE uuid=? AND account=?');
    $actorStmt->execute([$userUuid,$marker]);
    $actor=$actorStmt->fetch(PDO::FETCH_ASSOC);
    if (!$actor || (int)$actor['type']!==1 || (int)$actor['status']!==1
        || (string)$actor['name']!==$marker) {
        throw new RuntimeException('Synthetic actor readback failed');
    }
    $result['manifest']['user_id']=(int)$actor['id'];
    require_once $runtime['root'].'/application/base/Security.php';
    $token=\app\base\Security::encrypt(implode("\t",[
        (string)$actor['id'],$userUuid,$marker,'1','','',$marker,
    ]));
    if (!is_string($token) || $token==='') {
        throw new RuntimeException('Synthetic token unavailable');
    }
    $post=static function(string $path,array $form,string $auth='')
        use ($runtime): array {
        $allowed=[
            '/p/bill/lists',
            '/p/bill/look_bill_list',
            '/p/bill/timer_shaft',
            '/p/bill/recipeCalendarMarks',
        ];
        if (!in_array($path,$allowed,true)) {
            throw new RuntimeException('Non-whitelisted read handler');
        }
        $headers="Content-Type: application/x-www-form-urlencoded\r\n";
        if ($auth!=='') $headers.='token: '.$auth."\r\n";
        $context=stream_context_create(['http'=>[
            'method'=>'POST','header'=>$headers,
            'content'=>http_build_query($form,'','&',PHP_QUERY_RFC3986),
            'timeout'=>30,'ignore_errors'=>true,
        ]]);
        $body=@file_get_contents('http://127.0.0.1:'.$runtime['port']
            .$path,false,$context);
        $responseHeaders=$http_response_header??[];
        $status=preg_match('/^HTTP\/\S+\s+(\d+)/',
            (string)($responseHeaders[0]??''),$match)?(int)$match[1]:0;
        if (!is_string($body)) return [
            'http_status'=>$status,'transport'=>'incomplete','body_bytes'=>0,
            'body_sha256'=>null,'json'=>null,
        ];
        if (strlen($body)>8000000) return [
            'http_status'=>$status,'transport'=>'oversize',
            'body_bytes'=>strlen($body),'body_sha256'=>hash('sha256',$body),
            'json'=>null,
        ];
        $json=json_decode($body,true);
        return [
            'http_status'=>$status,
            'transport'=>is_array($json)?'json':'non-json',
            'body_bytes'=>strlen($body),'body_sha256'=>hash('sha256',$body),
            'json'=>is_array($json)?$json:null,
        ];
    };
    $phase='bill-permission-negative';
    $denied=$post('/p/bill/look_bill_list',[
        'menu_date'=>$date,'restaurant_id'=>$restaurantId,
    ],$token);
    if ($denied['transport']==='incomplete') $transportUncertain=true;
    if ($denied['http_status']!==200 || $denied['transport']!=='json'
        || ($denied['json']['code']??null)!==1) {
        throw new RuntimeException('No-menu Bill list denial failed');
    }
    $calendarDenied=$post('/p/bill/recipeCalendarMarks',[
        'restaurant_ids'=>json_encode([$restaurantId]),
        'meal_times'=>json_encode([$mealTimes]),
        'start_date'=>$date,'end_date'=>$date,
    ],$token);
    if ($calendarDenied['transport']==='incomplete') $transportUncertain=true;
    if ($calendarDenied['http_status']!==200
        || $calendarDenied['transport']!=='json'
        || ($calendarDenied['json']['code']??null)!==1) {
        throw new RuntimeException('No-menu calendar denial failed');
    }
    $result['permission_negative']=[
        'bill_list_without_menu_denied'=>true,
        'calendar_without_menu_denied'=>true,
    ];

    $phase='fixture-role-scope';
    $db->prepare('INSERT INTO ydy_role'
        .' (uuid,name,status,create_time,create_by) VALUES (?,?,1,?,?)')
        ->execute([$roleUuid,$marker,$now,$marker]);
    $roleStmt=$open()->prepare('SELECT id FROM ydy_role WHERE uuid=? AND name=?');
    $roleStmt->execute([$roleUuid,$marker]);
    $role=$roleStmt->fetch(PDO::FETCH_ASSOC);
    if (!$role) throw new RuntimeException('Synthetic role readback failed');
    $result['manifest']['role_id']=(int)$role['id'];
    $db->prepare('INSERT INTO ydy_user_role'
        .' (user_uuid,role_uuid,create_time,create_by) VALUES (?,?,?,?)')
        ->execute([$userUuid,$roleUuid,$now,$marker]);
    $db->prepare('INSERT INTO ydy_role_menu'
        .' (role_uuid,menu_uuid,create_time,create_by) VALUES (?,?,?,?)')
        ->execute([$roleUuid,$menuUuid,$now,$marker]);
    $db->prepare('INSERT INTO ydy_user_data'
        .' (user_uuid,data_uuid,type) VALUES (?,?,3)')
        ->execute([$userUuid,$restaurantUuid]);
    $fixtureCheck=$open();
    if ($scalar($fixtureCheck,'SELECT COUNT(*) FROM ydy_user_role'
            .' WHERE user_uuid=? AND role_uuid=? AND create_by=?',
            [$userUuid,$roleUuid,$marker])!==1
        || $scalar($fixtureCheck,'SELECT COUNT(*) FROM ydy_role_menu'
            .' WHERE role_uuid=? AND menu_uuid=? AND create_by=?',
            [$roleUuid,$menuUuid,$marker])!==1
        || $scalar($fixtureCheck,'SELECT COUNT(*) FROM ydy_user_data'
            .' WHERE user_uuid=? AND data_uuid=? AND type=3',
            [$userUuid,$restaurantUuid])!==1) {
        throw new RuntimeException('Permission fixture readback failed');
    }
    $result['permission_positive']=['type1_bill_menu_and_type3_scope'=>true];

    $cases=[
        ['lists','/p/bill/lists',
            ['menu_date'=>$date,'restaurant_id'=>$restaurantId],true],
        ['look_bill_list','/p/bill/look_bill_list',
            ['menu_date'=>$date,'restaurant_id'=>$restaurantId],true],
        ['timer_shaft','/p/bill/timer_shaft',
            ['restaurant_id'=>$restaurantId],true],
        ['recipeCalendarMarks','/p/bill/recipeCalendarMarks',
            ['restaurant_ids'=>json_encode([$restaurantId]),
                'meal_times'=>json_encode([$mealTimes]),
                'start_date'=>$date,'end_date'=>$date],true],
    ];
    $targetUuid=(string)$candidate['dishes_uuid'];
    $hasTargetDish=static function($node) use (&$hasTargetDish,$targetUuid): bool {
        if (!is_array($node)) return false;
        if (array_key_exists('dishes_uuid',$node)
            && (string)$node['dishes_uuid']===$targetUuid) return true;
        foreach ($node as $child) {
            if (is_array($child) && $hasTargetDish($child)) return true;
        }
        return false;
    };
    foreach ($cases as [$label,$path,$form,$needsAuth]) {
        $phase='http-'.$label;
        $response=$post($path,$form,$needsAuth?$token:'');
        $json=$response['json'];
        $data=is_array($json)?($json['data']??null):null;
        $evidence=[
            'route'=>$path,'http_status'=>$response['http_status'],
            'transport'=>$response['transport'],
            'body_bytes'=>$response['body_bytes'],
            'body_sha256'=>$response['body_sha256'],
            'business_code'=>is_array($json)?($json['code']??null):null,
            'data_type'=>gettype($data),
        ];
        if (is_array($data)) {
            $evidence['data_key_sha12']=substr(hash('sha256',
                json_encode(array_keys($data),JSON_UNESCAPED_UNICODE)),0,12);
            $evidence['data_count']=count($data);
        }
        $safeFields=['name','dishes_uuid','type','meal_times','file_path',
            'price','limit_num','menu_date','recipe_count','copyable_count',
            'restaurant_count','meal_count'];
        $fieldTypes=[];
        $walk=static function($node) use (&$walk,&$fieldTypes,$safeFields): void {
            if (!is_array($node)) return;
            foreach ($safeFields as $field) {
                if (array_key_exists($field,$node)) {
                    $fieldTypes[$field][gettype($node[$field])]=true;
                }
            }
            foreach ($node as $child) {
                if (is_array($child)) $walk($child);
            }
        };
        $walk($data);
        $evidence['safe_field_types']=[];
        foreach ($fieldTypes as $field=>$types) {
            $names=array_keys($types); sort($names);
            $evidence['safe_field_types'][$field]=$names;
        }
        $nonempty=false;
        if ($label==='lists') {
            $targetMeal=$data[$mealTimes]??$data[(string)$mealTimes]??null;
            $nonempty=is_array($targetMeal) && count($targetMeal)>0
                && $hasTargetDish($targetMeal);
            $evidence['candidate_dish_found']=$hasTargetDish($targetMeal);
        } elseif ($label==='look_bill_list') {
            $mealGroup=$data[$mealTimes-1]??null;
            $children=0;
            foreach ((array)$mealGroup as $category) {
                if (is_array($category) && isset($category['children'])
                    && is_array($category['children'])) {
                    $children+=count($category['children']);
                }
            }
            $nonempty=is_array($data) && $children>0
                && $hasTargetDish($mealGroup);
            $evidence['target_meal_children_count']=$children;
            $evidence['candidate_dish_found']=$hasTargetDish($mealGroup);
        } elseif ($label==='timer_shaft') {
            $dates=array_map(static function($item): string {
                return substr((string)$item,0,10);
            },(array)$data);
            $nonempty=is_array($data) && in_array($date,$dates,true)
                && count(array_unique($dates))===$sourceDistinctDates;
            $evidence['date_count']=count($dates);
            $evidence['date_value_types']=array_values(array_unique(
                array_map('gettype',(array)$data)));
        } elseif ($label==='recipeCalendarMarks') {
            $rows=[];
            foreach ((array)$data as $item) {
                if (is_array($item)
                    && substr((string)($item['menu_date']??''),0,10)===$date) {
                    $rows[]=$item;
                }
            }
            $calendarRow=count($rows)===1?$rows[0]:[];
            $nonempty=count($rows)===1
                && is_int($calendarRow['recipe_count']??null)
                && is_int($calendarRow['copyable_count']??null)
                && is_int($calendarRow['restaurant_count']??null)
                && is_int($calendarRow['meal_count']??null)
                && $calendarRow['recipe_count']===$sourceRecipeCount
                && $calendarRow['copyable_count']===$sourceCopyableCount
                && $calendarRow['restaurant_count']===1
                && $calendarRow['meal_count']===1;
            $evidence['target_date_rows']=count($rows);
        }
        $evidence['nonempty_business_result']=$nonempty;
        $evidence['limited_pass']=$response['http_status']===200
            && $response['transport']==='json'
            && is_array($json) && ($json['code']??null)===0 && $nonempty;
        $result['cases'][]=$evidence;
        if (!$evidence['limited_pass']) {
            if ($response['transport']==='incomplete') $transportUncertain=true;
            $result['stopped_early']=in_array($response['transport'],
                ['incomplete','oversize'],true)
                ? 'http-transport-boundary'
                : 'first-business-or-contract-failure';
            break;
        }
    }
    $phase='source-readback';
    $check=$open();
    $result['readback']=[
        'candidate_recipe_count_unchanged'=>$scalar($check,
            'SELECT COUNT(*) FROM ydy_recipes WHERE menu_date=?'
            .' AND restaurant_id=? AND meal_times=?',
            [$date,$restaurantId,$mealTimes])===$sourceRecipeCount,
        'actor_exists'=>$scalar($check,'SELECT COUNT(*) FROM ydy_user'
            .' WHERE uuid=? AND account=?',[$userUuid,$marker])===1,
        'synthetic_data_scope_exists'=>$scalar($check,
            'SELECT COUNT(*) FROM ydy_user_data'
            .' WHERE user_uuid=? AND data_uuid=? AND type=3',
            [$userUuid,$restaurantUuid])===1,
    ];
    $result['status']='readonly-http-executed';
} catch (Throwable $error) {
    $result['status']='blocked';
    $result['phase']=$phase;
    $result['error_class']=get_class($error);
    $result['error_code']=(string)$error->getCode();
} finally {
    if ($fixtureTouched) {
        try {
            $cleanup=$open();
            $scope=[
                ['ydy_user','uuid=?','uuid=? AND account=?',
                    [$userUuid],[$userUuid,$marker]],
                ['ydy_role','uuid=?','uuid=? AND name=?',
                    [$roleUuid],[$roleUuid,$marker]],
                ['ydy_user_role','user_uuid=?',
                    'user_uuid=? AND role_uuid=? AND create_by=?',
                    [$userUuid],[$userUuid,$roleUuid,$marker]],
                ['ydy_role_menu','role_uuid=?',
                    'role_uuid=? AND menu_uuid=? AND create_by=?',
                    [$roleUuid],[$roleUuid,$menuUuid,$marker]],
                ['ydy_user_data','user_uuid=?',
                    'user_uuid=? AND data_uuid=? AND type=3',
                    [$userUuid],[$userUuid,$restaurantUuid]],
            ];
            foreach ($scope as [$table,$allWhere,$ourWhere,$allArgs,$ourArgs]) {
                $all=$scalar($cleanup,'SELECT COUNT(*) FROM '.$table
                    .' WHERE '.$allWhere,$allArgs);
                $ours=$scalar($cleanup,'SELECT COUNT(*) FROM '.$table
                    .' WHERE '.$ourWhere,$ourArgs);
                if ($all!==$ours || $ours>1) {
                    throw new RuntimeException('Unknown permission association');
                }
            }
            $deleteExact=static function(PDO $conn,string $table,
                string $where,array $args): void {
                $stmt=$conn->prepare('DELETE FROM '.$table.' WHERE '.$where);
                $stmt->execute($args);
            };
            // Revokes this run's role/menu association, not the token itself.
            $deleteExact($cleanup,'ydy_role_menu',
                'role_uuid=? AND menu_uuid=? AND create_by=?',
                [$roleUuid,$menuUuid,$marker]);
            $deleteExact($cleanup,'ydy_user_role',
                'user_uuid=? AND role_uuid=? AND create_by=?',
                [$userUuid,$roleUuid,$marker]);
            $result['cleanup']['role_menu_links_revoked']=$scalar($open(),
                'SELECT COUNT(*) FROM ydy_role_menu WHERE role_uuid=?',
                [$roleUuid])===0
                && $scalar($open(),'SELECT COUNT(*) FROM ydy_user_role'
                    .' WHERE user_uuid=?',[$userUuid])===0;
            if (!$result['cleanup']['role_menu_links_revoked']) {
                throw new RuntimeException('Permission revocation readback failed');
            }
            if ($transportUncertain) {
                $currentCmd=str_replace("\0",' ',(string)file_get_contents(
                    '/proc/'.$runtime['pid'].'/cmdline'));
                $currentWorkers='';
                foreach (explode("\0",(string)file_get_contents(
                    '/proc/'.$runtime['pid'].'/environ')) as $entry) {
                    $parts=explode('=',$entry,2);
                    if (count($parts)===2
                        && $parts[0]==='PHP_CLI_SERVER_WORKERS') {
                        $currentWorkers=$parts[1];
                    }
                }
                if ($currentCmd!==$cmd
                    || strpos($currentCmd,' -S 127.0.0.1:'
                        .$runtime['port'].' ')===false
                    || strpos($currentCmd,$runtime['root'].'/public')===false
                    || substr_count($currentCmd,' -S ')!==1
                    || !in_array($currentWorkers,['','1'],true)) {
                    $result['cleanup']['remaining_fixture_retained']=true;
                    throw new RuntimeException('Single-worker barrier process changed');
                }
                $barrierRaw=@file_get_contents('http://127.0.0.1:'
                    .$runtime['port'].'/__poc_boundary',false,
                    stream_context_create(['http'=>[
                        'timeout'=>5,'ignore_errors'=>true,
                    ]]));
                $barrierHeaders=$http_response_header??[];
                $barrierStatus=preg_match('/^HTTP\/\S+\s+(\d+)/',
                    (string)($barrierHeaders[0]??''),$barrierMatch)
                    ? (int)$barrierMatch[1]:0;
                $barrier=json_decode((string)$barrierRaw,true);
                if ($barrierStatus!==200 || !is_array($barrier)
                    || empty($barrier['external_stubs'])
                    || empty($barrier['redis_isolated'])
                    || !empty($barrier['real_queue'])) {
                    $result['cleanup']['remaining_fixture_retained']=true;
                    throw new RuntimeException('Isolated HTTP barrier failed');
                }
                $result['cleanup']['barrier']='passed';
            }
            $deleteExact($cleanup,'ydy_user_data',
                'user_uuid=? AND data_uuid=? AND type=3',
                [$userUuid,$restaurantUuid]);
            $deleteExact($cleanup,'ydy_role','uuid=? AND name=?',
                [$roleUuid,$marker]);
            $deleteExact($cleanup,'ydy_user','uuid=? AND account=?',
                [$userUuid,$marker]);
            $independent=$open();
            $remaining=0;
            foreach ($scope as [$table,$allWhere,$ourWhere,$allArgs,$ourArgs]) {
                $remaining+=$scalar($independent,
                    'SELECT COUNT(*) FROM '.$table.' WHERE '.$allWhere,$allArgs);
            }
            $result['cleanup']['synthetic_five_tables_zero']=$remaining===0;
            $result['cleanup']['remaining_rows']=$remaining;
            if ($remaining!==0) throw new RuntimeException('Permission cleanup incomplete');
            $manifest['permission_fixture_phase']='cleaned';
            $written=file_put_contents($manifestPath,
                json_encode($manifest,JSON_UNESCAPED_SLASHES),LOCK_EX);
            if (!is_int($written) || $written<=0) {
                throw new RuntimeException('Manifest cleaned-state write failed');
            }
        } catch (Throwable $cleanupError) {
            $result['status']='cleanup-blocked';
            $result['cleanup']['error_class']=get_class($cleanupError);
            $result['cleanup']['error_code']=(string)$cleanupError->getCode();
        }
    }
}
$allFour=count($result['cases'])===4;
foreach ($result['cases'] as $case) {
    if (empty($case['limited_pass'])) $allFour=false;
}
$result['all_four_limited_pass']=$allFour;
$readbackPass=isset($result['readback'])
    && count($result['readback'])===3;
foreach ((array)($result['readback']??[]) as $passed) {
    if ($passed!==true) $readbackPass=false;
}
$result['all_source_readback_pass']=$readbackPass;
echo json_encode($result,JSON_UNESCAPED_SLASHES).PHP_EOL;
if ($result['status']!=='readonly-http-executed' || !$allFour
    || !$readbackPass
    || empty($result['cleanup']['synthetic_five_tables_zero'])) exit(1);
