<?php

declare(strict_types=1);

// Deliberately disabled by default. Only this script's permission fixture can
// change; business tables and four Bill HTTP handlers are read-only.
if ($argc !== 2 || $argv[1] !== '--run-approved-c-bill4'
    || getenv('CDX_APPROVED_C_BILL_FIXTURE') !== '1') {
    fwrite(STDERR, "Approval flag required; no request sent\n");
    exit(2);
}

$phase = 'runtime-boundary';
$output = ['lane'=>'C','cases'=>[],'source'=>'existing-natural-data'];
$marker='CDXPARBILL'.strtoupper(bin2hex(random_bytes(4)));
$newUuid=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=$newUuid();
$roleUuid=$newUuid();
$fixtureTouched=false;
$transportUncertain=false;
$db=null;
$billMenuUuid='';
$restaurantUuid='';
$open=null;
$scalar=null;
$output['manifest']=['marker'=>$marker,'user_uuid'=>$userUuid,
    'role_uuid'=>$roleUuid,'only_synthetic_identifiers'=>true];
try {
    $pid = (string)getenv('POC_RUNTIME_PID');
    if (!ctype_digit($pid) || (int)$pid <= 0) {
        throw new RuntimeException('Missing approved runtime PID');
    }
    $root = '/tmp/zhct-dm-poc-20260928.fcpiiN/store';
    $cmdline = str_replace("\0",' ',
        (string)file_get_contents('/proc/'.$pid.'/cmdline'));
    if (strpos($cmdline,'127.0.0.1:18080')===false
        || strpos($cmdline,$root.'/public')===false
        || substr_count($cmdline,' -S ')!==1) {
        throw new RuntimeException('Wrong single-process isolated Store runtime');
    }
    $runtime = [];
    foreach (explode("\0",(string)file_get_contents('/proc/'.$pid.'/environ')) as $entry) {
        $pair = explode('=',$entry,2);
        if (count($pair)===2 && in_array($pair[0],
            ['POC_HOST','POC_PORT','POC_SCHEMA','POC_USER','POC_PASSWORD',
                'PHP_CLI_SERVER_WORKERS'],true)) {
            $runtime[$pair[0]]=$pair[1];
        }
    }
    if (($runtime['POC_HOST']??'')!=='127.0.0.1'
        || ($runtime['POC_PORT']??'')!=='5237'
        || strtoupper((string)($runtime['POC_SCHEMA']??''))!=='ZHCT_APP'
        || strtoupper((string)($runtime['POC_USER']??''))!=='ZHCT_APP'
        || ($runtime['POC_PASSWORD']??'')===''
        || !in_array((string)($runtime['PHP_CLI_SERVER_WORKERS']??''),
            ['', '1'],true)) {
        throw new RuntimeException('Wrong nonproduction database boundary');
    }
    $hashGate = [
        'CDX_EXPECT_BILL_MODEL_SHA256'=>$root.'/application/model/Bill.php',
        'CDX_EXPECT_BILL_CONTROLLER_SHA256'=>$root.'/application/p/controller/Bill.php',
        'CDX_EXPECT_BILL_LOGIC_SHA256'=>$root.'/application/p/logic/Bill.php',
        'CDX_EXPECT_RECIPE_BATCH_SHA256'=>$root.'/application/p/logic/RecipeBatch.php',
    ];
    foreach ($hashGate as $key=>$file) {
        $expected=(string)getenv($key);
        if (!preg_match('/^[a-f0-9]{64}$/',$expected)
            || !hash_equals($expected,(string)hash_file('sha256',$file))) {
            throw new RuntimeException('Unapproved runtime source hash');
        }
    }

    $phase='poc-boundary';
    $boundaryBody=@file_get_contents('http://127.0.0.1:18080/__poc_boundary',
        false,stream_context_create(['http'=>['timeout'=>3]]));
    $boundaryHeaders=$http_response_header??[];
    $boundaryStatus=preg_match('/^HTTP\/\S+\s+(\d+)/',
        (string)($boundaryHeaders[0]??''),$boundaryMatch)
        ? (int)$boundaryMatch[1]:0;
    $boundary=json_decode((string)$boundaryBody,true);
    if ($boundaryStatus!==200 || !is_array($boundary)
        || empty($boundary['external_stubs'])
        || empty($boundary['redis_isolated']) || !empty($boundary['real_queue'])) {
        throw new RuntimeException('Isolated HTTP boundary failed');
    }

    $phase='connect';
    $open=static function() use ($runtime): PDO {
        return new PDO('dm:host=127.0.0.1;port=5237;schema=ZHCT_APP',
            $runtime['POC_USER'],$runtime['POC_PASSWORD'],[
                PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,
            ]);
    };
    $db=$open();
    if (strtoupper((string)$db->query(
        "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL"
    )->fetchColumn())!=='ZHCT_APP') {
        throw new RuntimeException('Wrong current schema');
    }
    $scalar=static function(PDO $conn,string $sql,array $args=[]): int {
        $stmt=$conn->prepare($sql);
        $stmt->execute($args);
        return (int)$stmt->fetchColumn();
    };

    $phase='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 USER_TAB_COLUMNS'
        .' WHERE TABLE_NAME=?');
    foreach ($businessRequired as $table=>$columns) {
        $businessColumns->execute([$table]);
        $actual=array_map('strtoupper',
            $businessColumns->fetchAll(PDO::FETCH_COLUMN));
        if (array_diff($columns,$actual)) {
            throw new RuntimeException('Bill business table column gate failed');
        }
    }

    $phase='permission-schema';
    $requiredPermissionColumns=[
        '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'],
    ];
    $insertedPermissionColumns=[
        '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'],
    ];
    $columnStmt=$db->prepare('SELECT COLUMN_NAME,NULLABLE,DATA_DEFAULT'
        .' FROM USER_TAB_COLUMNS WHERE TABLE_NAME=?');
    foreach ($requiredPermissionColumns as $table=>$required) {
        $columnStmt->execute([$table]);
        $rows=$columnStmt->fetchAll(PDO::FETCH_ASSOC);
        $columns=[];
        foreach ($rows as $row) {
            $row=array_change_key_case($row,CASE_LOWER);
            $columns[strtoupper((string)$row['column_name'])]=$row;
        }
        if (array_diff($required,array_keys($columns))) {
            throw new RuntimeException('Permission fixture column gate failed');
        }
        foreach ($columns as $name=>$column) {
            if ($name!=='ID' && (string)$column['nullable']==='N'
                && $column['data_default']===null
                && !in_array($name,$insertedPermissionColumns[$table],true)) {
                throw new RuntimeException('Permission fixture omitted required column');
            }
        }
        if ($scalar($db,'SELECT COUNT(*) FROM USER_TRIGGERS'
            .' WHERE TABLE_NAME=?',[$table])!==0
            || $scalar($db,'SELECT COUNT(*) FROM USER_CONSTRAINTS'
                .' WHERE TABLE_NAME=? AND CONSTRAINT_TYPE=\'R\'',[$table])!==0) {
            throw new RuntimeException('Permission fixture trigger/FK gate failed');
        }
    }
    $hasUnique=static function(PDO $conn,string $table,array $columns): bool {
        $stmt=$conn->prepare('SELECT i.INDEX_NAME,i.UNIQUENESS,'
            .' ic.COLUMN_NAME,ic.COLUMN_POSITION FROM USER_INDEXES i'
            .' JOIN USER_IND_COLUMNS ic ON i.INDEX_NAME=ic.INDEX_NAME'
            .' WHERE i.TABLE_NAME=? ORDER BY i.INDEX_NAME,ic.COLUMN_POSITION');
        $stmt->execute([$table]);
        $indexes=[];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $row=array_change_key_case($row,CASE_LOWER);
            $name=(string)$row['index_name'];
            $indexes[$name]['unique']=strtoupper((string)$row['uniqueness'])==='UNIQUE';
            $indexes[$name]['columns'][]=strtoupper((string)$row['column_name']);
        }
        foreach ($indexes as $index) {
            if ($index['unique'] && $index['columns']===$columns) return true;
        }
        return false;
    };
    if (!$hasUnique($db,'YDY_ROLE',['NAME'])
        || !$hasUnique($db,'YDY_USER_ROLE',['USER_UUID','ROLE_UUID'])
        || !$hasUnique($db,'YDY_ROLE_MENU',['ROLE_UUID','MENU_UUID'])
        || !$hasUnique($db,'YDY_USER_DATA',['USER_UUID','DATA_UUID','TYPE'])) {
        throw new RuntimeException('Permission fixture unique-key gate failed');
    }
    if ($scalar($db,'SELECT COUNT(*) FROM YDY_USER WHERE ACCOUNT LIKE ?',
            ['CDXPARBILL%'])!==0
        || $scalar($db,'SELECT COUNT(*) FROM YDY_ROLE WHERE NAME LIKE ?',
            ['CDXPARBILL%'])!==0
        || $scalar($db,'SELECT COUNT(*) FROM YDY_USER_ROLE WHERE CREATE_BY LIKE ?',
            ['CDXPARBILL%'])!==0
        || $scalar($db,'SELECT COUNT(*) FROM YDY_ROLE_MENU WHERE CREATE_BY LIKE ?',
            ['CDXPARBILL%'])!==0) {
        throw new RuntimeException('Prior recipe permission fixture residue');
    }

    $phase='bill-menu';
    $menuStmt=$db->prepare('SELECT UUID,FILTER FROM YDY_MENU'
        .' WHERE LOWER(FILTER) LIKE ?');
    $menuStmt->execute(['%p/bill/look_bill_list%']);
    $billMenu=[];
    foreach ($menuStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $row=array_change_key_case($row,CASE_LOWER);
        $filter='/'.trim(strtolower((string)$row['filter']),' /');
        if ($filter==='/p/bill/look_bill_list') {
            $billMenu[]=(string)$row['uuid'];
        }
    }
    if (count($billMenu)!==1) {
        throw new RuntimeException('Exact Bill menu absent or ambiguous');
    }
    $billMenuUuid=$billMenu[0];

    $phase='natural-relation';
    $candidateSql='SELECT d.UUID D_UUID,r.MENU_DATE MENU_DATE,'
        .' r.RESTAURANT_ID RESTAURANT_ID,r.MEAL_TIMES MEAL_TIMES,'
        .' st.UUID RESTAURANT_UUID'
        .' FROM YDY_DISHES d JOIN YDY_DISH c ON c.DISHES_UUID=d.UUID'
        .' JOIN YDY_FOODS_COMPOSITION f ON f.ID=c.COMPOSITION_ID'
        .' JOIN YDY_RECIPES r ON r.DISHES_UUID=d.UUID'
        .' JOIN YDY_RESTAURANTS st ON st.ID=r.RESTAURANT_ID'
        .' WHERE d.STATUS=1 AND d.IS_DEL=1 AND d.IS_DELETE=0'
        .' AND d.STORE_ID=10001 AND d.WEIGHT>0 AND d.ENERGY>0'
        .' AND d.CATEGORY BETWEEN 1 AND 6 AND c.IS_DELETE=0'
        .' AND f.IS_DELETE=0 AND st.IS_DELETE=2 AND st.STATUS=1'
        .' AND r.MEAL_TIMES BETWEEN 1 AND 4 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 FETCH FIRST 1 ROWS ONLY';
    $candidate=$db->query($candidateSql)->fetch(PDO::FETCH_ASSOC);
    $candidate=$candidate ? array_change_key_case($candidate,CASE_LOWER) : null;
    if (!$candidate || !preg_match('/^\d{4}-\d{2}-\d{2}/',
        (string)$candidate['menu_date'])) {
        throw new RuntimeException('Natural recipe/date-menu relation absent');
    }
    $restaurantUuid=(string)$candidate['restaurant_uuid'];
    if ($scalar($db,'SELECT COUNT(*) FROM YDY_USER_DATA'
        .' WHERE USER_UUID=?',[$userUuid])!==0
        || $scalar($db,'SELECT COUNT(*) FROM YDY_USER WHERE UUID=?',
            [$userUuid])!==0
        || $scalar($db,'SELECT COUNT(*) FROM YDY_ROLE WHERE UUID=?',
            [$roleUuid])!==0) {
        throw new RuntimeException('Random synthetic identity collision');
    }
    $date=substr((string)$candidate['menu_date'],0,10);
    $restaurantId=(int)$candidate['restaurant_id'];
    $mealTimes=(int)$candidate['meal_times'];
    if ($mealTimes<1 || $mealTimes>3) {
        throw new RuntimeException('Only PC meals 1-3 approved for Bill4');
    }
    $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');
    }
    $output['candidate']=[
        'relation_sha12'=>substr(hash('sha256',(string)$candidate['d_uuid'].'|'
            .$date.'|'.$restaurantId.'|'.$mealTimes),0,12),
        'menu_date_sha12'=>substr(hash('sha256',$date),0,12),
        'meal_times'=>$mealTimes,
        'source_recipe_count'=>$sourceRecipeCount,
        'source_visible_bill_count'=>$sourceVisibleCount,
        'source_copyable_count'=>$sourceCopyableCount,
        'source_distinct_dates'=>$sourceDistinctDates,
    ];

    $phase='recovery-manifest';
    $manifestDir='/tmp/zhct-store-bill4.ha6S87';
    if (!is_dir($manifestDir) || is_link($manifestDir)
        || (fileperms($manifestDir)&0777)!==0700) {
        throw new RuntimeException('Bill private manifest directory boundary failed');
    }
    $manifestPath='/tmp/zhct-store-bill4.ha6S87/manifest-'.$marker.'.json';
    $manifest=[
        'lane'=>'C','schema'=>'ZHCT_APP','marker'=>$marker,
        'user_uuid'=>$userUuid,'role_uuid'=>$roleUuid,
        'bill_menu_uuid'=>$billMenuUuid,
        'restaurant_uuid'=>$restaurantUuid,
        'business_tables_written'=>false,
        'permission_fixture_phase'=>'planned',
    ];
    $manifestFile=@fopen($manifestPath,'x');
    if (!$manifestFile) throw new RuntimeException('Private manifest create failed');
    chmod($manifestPath,0600);
    $manifestJson=json_encode($manifest,JSON_UNESCAPED_SLASHES);
    if (!is_string($manifestJson)
        || fwrite($manifestFile,$manifestJson)!==strlen($manifestJson)) {
        fclose($manifestFile);
        throw new RuntimeException('Private manifest write failed');
    }
    fclose($manifestFile);
    $output['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,UUID,ACCOUNT,NAME,TYPE,STATUS'
        .' FROM YDY_USER WHERE UUID=? AND ACCOUNT=?');
    $actorStmt->execute([$userUuid,$marker]);
    $actor=$actorStmt->fetch(PDO::FETCH_ASSOC);
    $actor=$actor ? array_change_key_case($actor,CASE_LOWER) : null;
    if (!$actor || (int)$actor['type']!==1 || (int)$actor['status']!==1
        || (string)$actor['name']!==$marker) {
        throw new RuntimeException('Synthetic type1 actor readback failed');
    }
    $output['manifest']['user_id']=(int)$actor['id'];

    $phase='synthetic-token';
    require_once $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=''): 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 HTTP route');
        }
        $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:18080'.$path,false,$context);
        $headersOut=$http_response_header??[];
        $status=preg_match('/^HTTP\/\S+\s+(\d+)/',(string)($headersOut[0]??''),$matches)
            ? (int)$matches[1] : 0;
        if (!is_string($body)) {
            return ['http_status'=>$status,'json'=>null,'body_bytes'=>0,
                'transport'=>'incomplete'];
        }
        if (strlen($body)>8000000) {
            return ['http_status'=>$status,'json'=>null,'body_bytes'=>strlen($body),
                'transport'=>'oversize'];
        }
        $decoded=json_decode($body,true);
        return ['http_status'=>$status,'json'=>is_array($decoded)?$decoded:null,
            'body_bytes'=>strlen($body),
            'transport'=>is_array($decoded)?'json':'non-json'];
    };
    $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');
    }
    $output['permission_negative']=[
        'bill_list_without_menu_denied'=>true,
        'calendar_without_menu_denied'=>true,
    ];

    $phase='fixture-role-and-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]);
    $roleRow=$roleStmt->fetch(PDO::FETCH_ASSOC);
    if (!$roleRow) throw new RuntimeException('Synthetic role readback failed');
    $roleRow=array_change_key_case($roleRow,CASE_LOWER);
    $output['manifest']['role_id']=(int)$roleRow['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,$billMenuUuid,$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,$billMenuUuid,$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 independent readback failed');
    }
    $output['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['d_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'],
            'business_code'=>is_array($json)?($json['code']??null):null,
            'data_type'=>gettype($data),
        ];
        if (is_array($data)) {
            // Hash only; keys may be business names and must not be printed.
            $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
                && $sourceVisibleCount>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') {
            $matches=[];
            foreach ((array)$data as $item) {
                if (is_array($item)
                    && substr((string)($item['menu_date']??''),0,10)===$date) {
                    $matches[]=$item;
                }
            }
            $calendarRow=count($matches)===1?$matches[0]:[];
            $nonempty=count($matches)===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($matches);
        }
        $evidence['nonempty_business_result']=$nonempty;
        $evidence['limited_pass']=$response['http_status']===200
            && $response['transport']==='json'
            && is_array($json) && ($json['code']??null)===0 && $nonempty;
        $output['cases'][]=$evidence;
        if (!$evidence['limited_pass']) {
            if ($response['transport']==='incomplete') $transportUncertain=true;
            $output['stopped_early']=in_array($response['transport'],
                ['incomplete','oversize'],true)
                ? 'http-transport-boundary'
                : 'first-business-or-contract-failure';
            break;
        }
    }
    $phase='source-readback';
    $check=$open();
    $output['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 TYPE=3 AND DATA_UUID=?',
            [$userUuid,(string)$candidate['restaurant_uuid']])===1,
    ];
    $output['status']='readonly-http-executed';
} catch (Throwable $error) {
    // Never print the exception message: some drivers include SQL/credentials.
    $output['status']='blocked';
    $output['phase']=$phase;
    $output['error_class']=get_class($error);
    $output['error_code']=(string)$error->getCode();
} finally {
    if ($fixtureTouched) {
        try {
            $cleanup=$open();
            $scopeChecks=[
                ['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,$billMenuUuid,$marker]],
                ['YDY_USER_DATA','USER_UUID=?',
                    'USER_UUID=? AND DATA_UUID=? AND TYPE=3',
                    [$userUuid],[$userUuid,$restaurantUuid]],
            ];
            foreach ($scopeChecks as [$table,$scope,$exact,$scopeArgs,$exactArgs]) {
                $all=$scalar($cleanup,'SELECT COUNT(*) FROM '.$table
                    .' WHERE '.$scope,$scopeArgs);
                $ours=$scalar($cleanup,'SELECT COUNT(*) FROM '.$table
                    .' WHERE '.$exact,$exactArgs);
                if ($all!==$ours || $ours>1) {
                    throw new RuntimeException('Unknown permission association; refuse cleanup');
                }
            }
            $deleteExact=static function(PDO $conn,string $table,
                string $where,array $args): void {
                $stmt=$conn->prepare('DELETE FROM '.$table.' WHERE '.$where);
                $stmt->execute($args);
            };
            // First revoke this test role's menu links. This is not token revocation.
            $deleteExact($cleanup,'YDY_ROLE_MENU',
                'ROLE_UUID=? AND MENU_UUID=? AND CREATE_BY=?',
                [$roleUuid,$billMenuUuid,$marker]);
            $deleteExact($cleanup,'YDY_USER_ROLE',
                'USER_UUID=? AND ROLE_UUID=? AND CREATE_BY=?',
                [$userUuid,$roleUuid,$marker]);
            $output['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 (!$output['cleanup']['role_menu_links_revoked']) {
                throw new RuntimeException('Permission revocation readback failed');
            }
            if ($transportUncertain) {
                $currentCmdline=str_replace("\0",' ',
                    (string)file_get_contents('/proc/'.$pid.'/cmdline'));
                $currentWorkerEnv='';
                foreach (explode("\0",(string)file_get_contents(
                    '/proc/'.$pid.'/environ')) as $entry) {
                    $pair=explode('=',$entry,2);
                    if (count($pair)===2 && $pair[0]==='PHP_CLI_SERVER_WORKERS') {
                        $currentWorkerEnv=$pair[1];
                    }
                }
                if ($currentCmdline!==$cmdline
                    || strpos($currentCmdline,'127.0.0.1:18080')===false
                    || strpos($currentCmdline,$root.'/public')===false
                    || substr_count($currentCmdline,' -S ')!==1
                    || !in_array($currentWorkerEnv,['','1'],true)) {
                    $output['cleanup']['barrier']='process-boundary-failed';
                    $output['cleanup']['remaining_fixture_retained']=true;
                    throw new RuntimeException('Isolated one-worker process changed');
                }
                $boundaryContext=stream_context_create(['http'=>['timeout'=>5]]);
                $barrier=@file_get_contents(
                    'http://127.0.0.1:18080/__poc_boundary',false,$boundaryContext);
                $barrierHeaders=$http_response_header??[];
                $barrierStatus=preg_match('/^HTTP\/\S+\s+(\d+)/',
                    (string)($barrierHeaders[0]??''),$barrierMatch)
                    ? (int)$barrierMatch[1]:0;
                $barrierJson=json_decode((string)$barrier,true);
                if ($barrierStatus!==200 || !is_array($barrierJson)
                    || empty($barrierJson['external_stubs'])
                    || empty($barrierJson['redis_isolated'])
                    || !empty($barrierJson['real_queue'])) {
                    $output['cleanup']['barrier']='failed';
                    $output['cleanup']['remaining_fixture_retained']=true;
                    throw new RuntimeException('Isolated one-worker HTTP barrier failed');
                }
                $output['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 ($scopeChecks as [$table,$scope,$exact,$scopeArgs,$exactArgs]) {
                $remaining+=$scalar($independent,
                    'SELECT COUNT(*) FROM '.$table.' WHERE '.$scope,$scopeArgs);
            }
            $output['cleanup']['synthetic_five_tables_zero']=$remaining===0;
            $output['cleanup']['remaining_rows']=$remaining;
            if ($remaining!==0) throw new RuntimeException('Fixture cleanup incomplete');
            $manifest['permission_fixture_phase']='cleaned';
            file_put_contents($manifestPath,
                json_encode($manifest,JSON_UNESCAPED_SLASHES),LOCK_EX);
        } catch (Throwable $cleanupError) {
            $output['status']='cleanup-blocked';
            $output['cleanup']['error_class']=get_class($cleanupError);
            $output['cleanup']['error_code']=(string)$cleanupError->getCode();
        }
    }
}
$allFourPass=count($output['cases'])===4;
foreach ($output['cases'] as $case) {
    if (empty($case['limited_pass'])) $allFourPass=false;
}
$output['all_four_limited_pass']=$allFourPass;
echo json_encode($output,JSON_UNESCAPED_SLASHES).PHP_EOL;
if ($output['status']!=='readonly-http-executed' || !$allFourPass
    || empty($output['cleanup']['synthetic_five_tables_zero'])) exit(1);
