<?php
// Offline tests only. This file never loads config, PDO, HTTP, or shell tools.
require __DIR__ . '/runner.php';
function pp_test_rows($plan) {
    $rows=[];
    foreach ([1,2,3,4] as $id) {
        $r=['id'=>$id,'lease_id'=>$plan['marker'],'meal_order_id'=>0,'meal_order_no'=>'',
            'user_id'=>0,'update_by'=>'leaseA','raw_request_a'=>null,
            'expire_time'=>'2000-01-01 00:00:00','create_time'=>'2001-01-01 00:00:00',
            'update_time'=>'2001-01-01 00:00:00'];
        foreach (['id','meal_order_id','user_id'] as $k) { $r['h_'.$k]=(string)$r[$k]; }
        foreach (['lease_id','meal_order_no','update_by','raw_request_a'] as $k) { $r['h_'.$k]=$r[$k]===null?null:strtoupper(bin2hex($r[$k])); }
        foreach (['expire_time','create_time','update_time'] as $k) { $r['f_'.$k]=$r[$k].'.000000'; }
        $r['clock0']='2026-10-11 09:00:00'; $r['clock6']='2026-10-11 09:00:00.720472'; $rows[]=$r;
    }
    return $rows;
}
$plan = json_decode(file_get_contents(__DIR__ . '/plan.json'), true);
$count = 0;
$assert = static function ($ok, $message) use (&$count) {
    ++$count;
    if (!$ok) { throw new RuntimeException($message); }
};
$reject = static function ($call, $name) use ($assert) {
    try { $call(); } catch (Throwable $e) { $assert(true, $name); return; }
    $assert(false, 'Did not reject: ' . $name);
};
$stats = pp_gate_plan($plan);
$assert($stats === ['read'=>120, 'DML'=>28, 'DDL'=>2], 'Exact literal ledger');
$assert($plan['budget']['read'] <= 150, 'Read cap');
foreach ($plan['steps'] as $index => $step) {
    $state = ['cursor'=>$index, 'read'=>0, 'DML'=>0, 'DDL'=>0, 'created'=>true,
        'collision_zero'=>true, 'drop_proof'=>true];
    if ($step['role'] === 'create') { $state['created'] = false; }
    pp_gate_step($plan, $step, $state);
    $assert(true, 'Literal step accepted');
}
$updateIndex = null; $createIndex = null; $dropIndex = null;
foreach ($plan['steps'] as $i => $step) {
    if ($step['role'] === 'mutation' && $updateIndex === null) { $updateIndex = $i; }
    if ($step['role'] === 'create') { $createIndex = $i; }
    if ($step['role'] === 'drop') { $dropIndex = $i; }
}
$step = $plan['steps'][$updateIndex];
$state = ['cursor'=>$updateIndex,'read'=>0,'DML'=>0,'DDL'=>0,'created'=>true,
    'collision_zero'=>true,'drop_proof'=>false];
foreach (['business_table','no_where','wrong_id','wrong_bind','wrong_type','extra_SQL','string_NULL','time_assignment'] as $kind) {
    $bad = $step;
    if ($kind === 'business_table') { $bad['SQL'] = str_replace('cdx_pput261011_s1','ydy_prepay_order',$bad['SQL']); }
    if ($kind === 'no_where') { $bad['SQL'] = preg_replace('/ WHERE .*/', '', $bad['SQL']); }
    if ($kind === 'wrong_id') { $bad['binds'][4] = 2; }
    if ($kind === 'wrong_bind') { $bad['binds'][0] = 999; }
    if ($kind === 'wrong_type') { $bad['PDO_types'][0] = 2; }
    if ($kind === 'extra_SQL') { $bad['SQL'] .= '; SELECT 1'; }
    if ($kind === 'string_NULL') { $bad['binds'][0] = 'NULL'; }
    if ($kind === 'time_assignment') { $bad['SQL'] = str_replace(' SET ', ' SET UPDATE_TIME=CURRENT_TIMESTAMP,', $bad['SQL']); }
    $reject(static function () use ($plan,$bad,$state) { pp_gate_step($plan,$bad,$state); }, $kind);
}
$badState = $state; $badState['DML'] = 28;
$reject(static function () use ($plan,$step,$badState) { pp_gate_step($plan,$step,$badState); }, 'DML cap');
$badState = $state; $badState['cursor'] = $updateIndex - 1;
$reject(static function () use ($plan,$step,$badState) { pp_gate_step($plan,$step,$badState); }, 'replay or out of order');
$create = $plan['steps'][$createIndex];
$badState = ['cursor'=>$createIndex,'read'=>0,'DML'=>0,'DDL'=>0,'created'=>true,'collision_zero'=>true,'drop_proof'=>false];
$reject(static function () use ($plan,$create,$badState) { pp_gate_step($plan,$create,$badState); }, 'CREATE replay');
$badState['created'] = false; $badState['collision_zero'] = false;
$reject(static function () use ($plan,$create,$badState) { pp_gate_step($plan,$create,$badState); }, 'CREATE collision');
$drop = $plan['steps'][$dropIndex];
$badState = ['cursor'=>$dropIndex,'read'=>0,'DML'=>0,'DDL'=>1,'created'=>true,'collision_zero'=>true,'drop_proof'=>false];
$reject(static function () use ($plan,$drop,$badState) { pp_gate_step($plan,$drop,$badState); }, 'unknown DROP ownership');
foreach (['budget','missing_read','wrong_lease','C_lane','unknown_read','wrong_columns'] as $kind) {
    $bad = $plan;
    if ($kind === 'budget') { $bad['budget']['read'] = 151; }
    if ($kind === 'missing_read') { array_pop($bad['steps']); }
    if ($kind === 'wrong_lease') { $bad['marker'] = 'OTHER'; }
    if ($kind === 'C_lane') { $bad['lane'] = 'C'; }
    if ($kind === 'unknown_read') { $bad['steps'][0]['SQL'] = 'SELECT * FROM ydy_user'; }
    if ($kind === 'wrong_columns') { array_pop($bad['columns']); }
    $reject(static function () use ($bad) { pp_gate_plan($bad); }, $kind);
}
$rows = pp_test_rows($plan);
pp_assert_snapshot($rows, [1,2,3,4], $plan);
$assert(true, 'Four full native rows and helper bytes');
foreach (['foreign_lease','foreign_row','lost_field','altered_hex','lost_fraction'] as $kind) {
    $bad = $rows;
    if ($kind === 'foreign_lease') { $bad[0]['lease_id'] = 'OTHER'; }
    if ($kind === 'foreign_row') { $bad[3]['id'] = 99; }
    if ($kind === 'lost_field') { unset($bad[0]['raw_request_a']); }
    if ($kind === 'altered_hex') { $bad[0]['h_lease_id'] = '00'; }
    if ($kind === 'lost_fraction') { $bad[0]['f_update_time'] = '2001-01-01 00:00:00.00000'; }
    $reject(static function () use ($bad,$plan) { pp_assert_snapshot($bad,[1,2,3,4],$plan); }, $kind);
}
// Unknown source-oracle vectors must accept either preserved or local-clock time.
foreach (['CLOB_SAME','ALL9_SELF'] as $label) {
    $before = $rows;
    if ($label === 'CLOB_SAME') {
        $before[0]['raw_request_a']='原始A';
        $before[0]['h_raw_request_a']=strtoupper(bin2hex('原始A'));
    }
    $after = $before;
    pp_check_transition($before,$after,$label,'mutation',true,$plan);
    $after[0]['update_time'] = '2026-10-11 09:00:00';
    $after[0]['f_update_time'] = '2026-10-11 09:00:00.000000';
    pp_check_transition($before,$after,$label,'mutation',true,$plan);
    $assert(true, $label . ' has no invented no-op oracle');
}
$reject(static function () use ($rows,$plan) { pp_check_transition($rows,$rows,'EXPLICIT_NULL_CHANGED','reset',false,$plan); }, 'Failed reset never becomes NULL oracle');
function pp_test_refresh($row) {
    foreach (['id','meal_order_id','user_id'] as $k) { $row['h_'.$k]=(string)$row[$k]; }
    foreach (['lease_id','meal_order_no','update_by','raw_request_a'] as $k) { $row['h_'.$k]=$row[$k]===null?null:strtoupper(bin2hex($row[$k])); }
    foreach (['expire_time','create_time','update_time'] as $k) { $row['f_'.$k]=$row[$k].'.000000'; }
    return $row;
}
foreach ($plan['cases'] as $case) {
    $reset=$rows;
    foreach (array_slice($plan['columns'],1) as $i=>$column) { $reset[0][strtolower($column)]=$case['reset_binds'][$i]; }
    $reset[0]=pp_test_refresh($reset[0]);
    pp_assert_snapshot($reset,[1,2,3,4],$plan);
    pp_check_transition($rows,$reset,$case['label'],'reset',true,$plan);
    $after=$reset;
    if ($case['label']==='CAS4') { $after[0]['meal_order_id']=7; $after[0]['meal_order_no']='CDXPPUT261011S1_ORDER'; $after[0]['user_id']=8; $after[0]['update_by']='miniapp'; }
    if ($case['label']==='VARCHAR_CASE') { $after[0]['update_by']='leasea'; }
    if (strpos($case['label'],'CLOB_')===0) { $after[0]['raw_request_a']=$case['binds'][0]; }
    if (strpos($case['label'],'EXPLICIT_')===0) { $after[0]['meal_order_id']=7; }
    if ($case['label']==='EXPLICIT_VALUE_CHANGED') { $after[0]['update_time']='2001-01-01 09:07:26'; }
    elseif ($case['label']==='EXPLICIT_NULL_CHANGED') { $after[0]['update_time']='0000-00-00 00:00:00'; }
    elseif (!in_array($case['label'],['NOOP_SCALAR','EXPLICIT_SELF_CHANGED','CLOB_SAME','ALL9_SELF'],true)) { $after[0]['update_time']=$after[0]['clock0']; }
    $after[0]=pp_test_refresh($after[0]);
    pp_assert_snapshot($after,[1,2,3,4],$plan);
    pp_check_transition($reset,$after,$case['label'],'mutation',true,$plan);
    $assert(true,'Full literal reset and mutation witness '.$case['label']);
    $bad=$after; $bad[3]['update_by']='FOREIGN';
    $reject(static function () use ($reset,$bad,$case,$plan) { pp_check_transition($reset,$bad,$case['label'],'mutation',true,$plan); }, 'Protected ID4 delta '.$case['label']);
}
pp_check_transition($rows,$rows,'EXPLICIT_NULL_CHANGED','mutation',false,$plan);
$assert(true,'NULL native failure branch preserves raw oracle');
$beforeHash=pp_row_digests($rows,$plan);
$changed=$rows; $changed[0]['update_time']='2001-01-01 00:00:01';
$assert(pp_row_digests($changed,$plan)!==$beforeHash,'Full10 digest detects time delta');
$changed=$rows; $changed[0]['id']='1';
$assert(pp_row_digests($changed,$plan)!==$beforeHash,'Full10 digest detects native PHP type delta');
$schema=['columns'=>[],'indexes'=>[['index_name'=>'PRIMARY','column_name'=>'ID','non_unique'=>0]],
    'constraints'=>[['constraint_type'=>'PRIMARY KEY']],'keys'=>[['referenced_table_name'=>null]],
    'table'=>[['engine'=>'InnoDB']],'ddl'=>[['create table'=>'original literal test definition']],
    'triggers'=>[],'views'=>[],'routines'=>[]];
$types=['int(11)','varchar(32)','int(10) unsigned','varchar(32)','int(10) unsigned','varchar(32)','text','datetime','datetime','datetime'];
$defaults=[null,null,'0','','0','',null,'2000-01-01 00:00:00','CURRENT_TIMESTAMP','CURRENT_TIMESTAMP'];
foreach ($plan['columns'] as $i=>$column) {
    $text=in_array($i,[1,3,5,6],true);
    $schema['columns'][]=['column_name'=>$column,'column_type'=>$types[$i],'ordinal_position'=>$i+1,
        'column_default'=>$defaults[$i],'is_nullable'=>$i===6?'YES':'NO','datetime_precision'=>$i>=7?0:null,
        'extra'=>$i===9?'on update CURRENT_TIMESTAMP':'','character_set_name'=>$text?'utf8mb4':null,
        'collation_name'=>$text?'utf8mb4_general_ci':null,'character_maximum_length'=>$text?($i===6?65535:32):null,
        'character_octet_length'=>$text?($i===6?65535:128):null];
}
pp_verify_schema($schema,$plan); $assert(true,'Complete source schema fixture accepted');
foreach (['type','fsp','nullability','default','extra_index','incoming_FK','trigger','view','routine'] as $kind) {
    $bad=$schema;
    if ($kind==='type') { $bad['columns'][2]['column_type']='int(11)'; }
    if ($kind==='fsp') { $bad['columns'][9]['datetime_precision']=6; }
    if ($kind==='nullability') { $bad['columns'][6]['is_nullable']='NO'; }
    if ($kind==='default') { $bad['columns'][9]['column_default']='2001-01-01 00:00:00'; }
    if ($kind==='extra_index') { $bad['indexes'][]=$bad['indexes'][0]; }
    if ($kind==='incoming_FK') { $bad['keys'][]=['referenced_table_name'=>$plan['table']]; }
    if ($kind==='trigger') { $bad['triggers'][]=['trigger_name'=>'UNKNOWN']; }
    if ($kind==='view') { $bad['views'][]=['table_name'=>'UNKNOWN']; }
    if ($kind==='routine') { $bad['routines'][]=['routine_name'=>'UNKNOWN']; }
    $reject(static function () use ($bad,$plan) { pp_verify_schema($bad,$plan); },'Unknown cleanup schema/dependency '.$kind);
}
echo json_encode(['status'=>'PURE_GATE_TESTS_PASS','assertions'=>$count,
    'statement_instances'=>150,'reads'=>120,'DML'=>28,'DDL'=>2,
    'actual_DB_connections'=>0,'actual_SQL'=>0,'actual_HTTP'=>0]), "\n";
