<?php
define('ABSPATH', __DIR__);
#[AllowDynamicProperties] class WC_Payment_Gateway {
    public $settings;
    public function init_settings() { $this->settings = $GLOBALS['settings']; }
    public function get_option($k,$d='') { return isset($this->settings[$k]) ? $this->settings[$k] : $d; }
    public function is_available(){return true;}
    public function get_return_url($o){return 'https://shop.test/complete/'.$o->get_id();}
}
class WP_Error {private $c,$m,$d;function __construct($c,$m,$d=null){$this->c=$c;$this->m=$m;$this->d=$d;}function get_error_code(){return $this->c;}function get_error_message(){return $this->m;}function get_error_data(){return $this->d;}}
function wp_parse_url($s,$c=-1){return parse_url($s,$c);}
function wp_generate_uuid4(){static $i=0;return sprintf('00000000-0000-4000-8000-%012d',++$i);}
function __($s,$domain=''){return $s;} function add_action(){} function wc_get_logger(){return new class {function log(){}};}
function untrailingslashit($s){return rtrim($s,'/');} function mochipay_wc_payment_method_options(){return array('USDT_TRC20'=>'USDT','BTC_BITCOIN'=>'BTC');}
function mochipay_wc_enabled_payment_methods($s){return array('USDT_TRC20','BTC_BITCOIN');}
function add_option($k,$v){if(isset($GLOBALS['locks'][$k]))return false;$GLOBALS['locks'][$k]=$v;return true;}
function delete_option($k){unset($GLOBALS['locks'][$k]);} function wc_add_notice($s,$type){$GLOBALS['notices'][]=$s;}
function wc_get_order($id){return $GLOBALS['orders'][$id]??false;} function wc_clean($s){return $s;}
function wp_unslash($s){return $s;} function sanitize_text_field($s){return strip_tags((string)$s);} function esc_url_raw($s){return $s;}
function is_wp_error($s){return $s instanceof WP_Error;} function wp_json_encode($v,$flags=0){return json_encode($v,$flags);}
function wp_strip_all_tags($s){return strip_tags($s);} function home_url($s){return 'https://shop.test'.$s;}
function wc_get_checkout_url(){return 'https://shop.test/checkout/';} function absint($s){return abs((int)$s);}
function add_query_arg($a,$url){return $url.'?'.http_build_query($a);}
function WC(){static $wc; if(!$wc)$wc=new class{public $cart,$gateways;function __construct(){$this->cart=new class{function empty_cart(){}};}function payment_gateways(){return $this->gateways;}function api_request_url($n){return 'https://shop.test/wc-api/'.$n;}};return $wc;}
function wp_remote_request($url,$args){$GLOBALS['requests'][]=array($url,$args);return array_shift($GLOBALS['responses']);}
function wp_remote_retrieve_response_code($r){return $r['code'];} function wp_remote_retrieve_body($r){return $r['body'];}
class TestOrder {
    public $meta=array(), $status='pending', $notes=array(), $payments=0;
    function __construct(public $id){}
    function get_id(){return $this->id;}function get_payment_method(){return 'mochipay';}function get_checkout_order_received_url(){return 'https://shop.test/complete/'.$this->id;}function get_order_key(){return 'wc_key_'.$this->id;}
    function get_order_number(){return $this->id;}function get_total(){return '10.00';}function get_currency(){return 'USD';}
    function get_meta($k){return $this->meta[$k]??'';}function update_meta_data($k,$v){$this->meta[$k]=$v;}function delete_meta_data($k){unset($this->meta[$k]);}
    function get_address($type){return array();}function get_items($type=null){return array();}function get_billing_email(){return 'a@example.test';}
    function get_billing_phone(){return '';}function get_customer_ip_address(){return '127.0.0.1';}
    function add_order_note($n){$this->notes[]=$n;}function save(){}function is_paid(){return $this->status==='processing';}
    function has_status($s){return in_array($this->status,(array)$s,true);}function update_status($s,$n){$this->status=$s;}
    function payment_complete($tx){$this->payments++;$this->status='processing';}
}
require __DIR__.'/../includes/class-mochipay-payment-data.php';
require __DIR__.'/../includes/class-wc-gateway-mochipay.php';
$GLOBALS['settings']=array('api_base_url'=>'https://mochi.bz','api_key'=>'test','api_secret'=>'test-secret','checkout_mode'=>'onsite');
$GLOBALS['locks']=array();$GLOBALS['requests']=array();$GLOBALS['responses']=array();
$g=new WC_Gateway_MochiPay();$checks=0;
function check($v,$msg){global $checks;$checks++;if(!$v)throw new Exception('FAIL: '.$msg);echo "PASS $msg\n";}
foreach(array('10.00'=>'10','0.000000000000000001'=>'0.000000000000000001','1.234567890123456789'=>'1.234567890123456789','9e-18'=>'0.000000000000000009','1e2'=>'100','001.5000'=>'1.5','-0.000'=>'0') as $a=>$b){check(MochiPay_Payment_Data::decimal($a)===$b,'decimal '.$a);}
check(MochiPay_Payment_Data::decimal('NaN')==='','reject non-decimal');
$o=new TestOrder(101);$GLOBALS['orders'][101]=$o;
$ref='WC-'.strtoupper(substr(md5('https://shop.test/'),0,8)).'-101';
$response=array('success'=>true,'order_id'=>str_repeat('a',32),'merchant_order_id'=>$ref,'amount'=>10,'currency'=>'USD','payment_method'=>'USDT_TRC20','status'=>'WAITING_PAYMENT','pay_amount'=>'10.000000000000000009','payment_address'=>'TQGR6a9KRSymkUbqA6RtLCvzcTqAQAsfxu','payment_url'=>'https://mochi.bz/pay/'.str_repeat('a',32),'expires_at'=>'2026-10-03 23:00:00');
$GLOBALS['responses'][]=array('code'=>200,'body'=>str_replace('"10.000000000000000009"','10.000000000000000009',json_encode($response)));
$result=$g->process_payment(101);
check($result['result']==='success' && strpos($result['redirect'],'https://shop.test/checkout/')===0,'onsite redirect stays on merchant site');
check(strpos($result['redirect'],'mochipay_pay=101')!==false,'resume link includes order ID');
check($o->meta['_mochipay_payment_snapshot']['pay_amount']==='10.000000000000000009','JSON decimal lexeme survives decoding');
$request=$GLOBALS['requests'][0];$body=$request[1]['body'];
check(json_decode($body,true)['unique_amount_direction']==='UP','configured unique amount direction sent');
check($request[1]['headers']['X-Mochi-Signature']===base64_encode(hash_hmac('sha256',$body,'test-secret',true)),'create request signature');
check($request[1]['redirection']===0 && $request[1]['sslverify'],'no credential redirect and TLS verification enabled');
$count=count($GLOBALS['requests']);$g->process_payment(101);
check(count($GLOBALS['requests'])===$count,'reopen does not create another payment');
check(!$o->get_meta('_mochipay_create_uncertain'),'successful creation clears uncertain flag');
check(!isset($GLOBALS['locks']['mochipay_create_101']),'creation lock released');
$o->meta['_mochipay_payment_snapshot']=$response;
check(MochiPay_Payment_Data::matches($response,$o,$ref),'valid order binding');
foreach(array('order_id'=>'bad','merchant_order_id'=>'bad','currency'=>'EUR','amount'=>'11','payment_method'=>'BTC_BITCOIN','payment_address'=>'TWrongAddress12345678901234567890','pay_amount'=>'10.000000000000000008') as $field=>$bad){$d=$response;$d[$field]=$bad;check(!MochiPay_Payment_Data::matches($d,$o,$ref),'reject changed '.$field);}
$d=$response;unset($d['payment_method']);$d['wallet_type']='USDT';$d['network']='TRC20';
check(MochiPay_Payment_Data::matches($d,$o,$ref),'query wallet_type plus network binding');
$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($d));$verified=$g->verified_order($o);
check(!is_wp_error($verified),'signed query verifies existing payment');
$queryRequest=end($GLOBALS['requests']);$query='order_id='.str_repeat('a',32);
check($queryRequest[1]['headers']['X-Mochi-Signature']===base64_encode(hash_hmac('sha256',$query,'test-secret',true)),'query signature uses raw query text');
$v=MochiPay_Payment_Data::view($d,$o);check($v['payAmount']==='10.000000000000000009','view retains payable precision');
check(!isset($v['customer_email'],$v['api_key'],$v['api_secret']),'public view excludes customer and secret data');
$d['status']='UNDERPAID';check(MochiPay_Payment_Data::view($d,$o)['status']==='UNDERPAID','underpayment stays underpaid');
$d['status']='OVERPAID';check(MochiPay_Payment_Data::view($d,$o)['status']==='OVERPAID','overpayment requires server review');
$d['status']='UNKNOWN';check(is_wp_error(MochiPay_Payment_Data::view($d,$o)),'unknown status rejected');
$d['status']='WAITING_PAYMENT';$d['updated_at']='2026-10-04 00:00:00';check(MochiPay_Payment_Data::view($d,$o)['status']==='EXPIRED','expiry compares server-origin timestamps');
$GLOBALS['settings']['checkout_mode']='hpp';$hpp=new WC_Gateway_MochiPay();check($hpp->payment_redirect($o)===$response['payment_url'],'HPP keeps hosted checkout URL');
$o2=new TestOrder(102);$GLOBALS['orders'][102]=$o2;$GLOBALS['responses'][]=new WP_Error('timeout','timeout');
check($g->process_payment(102)['result']==='failure','network error returns checkout failure');
$count=count($GLOBALS['requests']);$GLOBALS['responses'][]=new WP_Error('query','not found');$g->process_payment(102);check(count($GLOBALS['requests'])===$count+1 && end($GLOBALS['requests'])[1]['method']==='GET','uncertain create is queried without another POST');
$recovered=$response;$recovered['merchant_order_id']=str_replace('-101','-102',$ref);$recovered['order_id']=str_repeat('b',32);$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($recovered));check($g->process_payment(102)['result']==='success' && $o2->get_meta('_mochipay_order_id')===str_repeat('b',32),'uncertain create recovers existing payment by merchant reference');
$GLOBALS['locks']['mochipay_create_103']=time();$o3=new TestOrder(103);$GLOBALS['orders'][103]=$o3;check($g->process_payment(103)['result']==='failure','concurrent create rejected');
$o->status='processing';check($g->process_payment(101)['redirect']==='https://shop.test/complete/101','paid order returns completion link');
$o->status='cancelled';check($g->process_payment(101)['result']==='failure','cancelled order cannot reopen payment');
// Exercise the actual AJAX controller with WordPress response functions as exits.
class JsonExit extends Exception {function __construct(public $payload,public $ok,public $http=200){parent::__construct();}}
function nocache_headers(){}function check_ajax_referer(){return $GLOBALS['nonce_ok']??true;}
function wp_send_json_error($a,$http=200){throw new JsonExit($a,false,$http);}
function wp_send_json_success($a){throw new JsonExit($a,true);}
function get_transient($k){return $GLOBALS['cache'][$k]??false;}function set_transient($k,$v,$t){$GLOBALS['cache'][$k]=$v;}
function delete_transient($k){unset($GLOBALS['cache'][$k]);}
require __DIR__.'/../includes/class-mochipay-onsite.php';
$wc=WC();$wc->gateways=new class($g){function __construct(public $g){}function payment_gateways(){return array('mochipay'=>$this->g);}};
// The fixture above supplies WC(); install its payment_gateways method through a wrapper.
function run_ajax(){try{MochiPay_Onsite::ajax();}catch(JsonExit $x){return $x;}throw new Exception('AJAX did not return JSON');}
$_POST=array('order_id'=>101,'key'=>'wrong');$GLOBALS['nonce_ok']=false;
check(run_ajax()->http===403,'AJAX rejects expired nonce');$GLOBALS['nonce_ok']=true;
check(run_ajax()->http===403,'AJAX rejects wrong order key');
$_POST['key']=$o->get_order_key();$o->status='pending';$GLOBALS['cache']=array();
$d=$response;unset($d['payment_method']);$d['wallet_type']='USDT';$d['network']='TRC20';
$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($d));$r=run_ajax();
check($r->ok && $r->payload['status']==='WAITING_PAYMENT','AJAX returns bound waiting payment');
$count=count($GLOBALS['requests']);run_ajax();check(count($GLOBALS['requests'])===$count,'AJAX throttles upstream query via eight-second cache');
$GLOBALS['cache']=array();$d['status']='PAID';$d['tx_hash']='tx-confirmed';$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($d));
$r=run_ajax();check($r->ok && $r->payload['status']==='PAID','AJAX settles authenticated PAID result');
check($o->payments===1,'payment completion runs once');run_ajax();check($o->payments===1,'repeated poll does not complete payment twice');
check(!isset($GLOBALS['locks']['mochipay_settle_101']),'settlement lock released');
$o->status='cancelled';check(run_ajax()->http===409,'AJAX refuses cancelled order');
$o->status='pending';$GLOBALS['cache']=array();$d['status']='UNDERPAID';$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($d));
check(run_ajax()->payload['status']==='UNDERPAID' && !$o->is_paid(),'AJAX does not settle underpayment');
$GLOBALS['cache']=array();$d['status']='PAID';$d['amount']='999';$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($d));
check(!run_ajax()->ok && !$o->is_paid(),'AJAX rejects mismatched paid amount');
// Replaying requires an explicitly verified server and the exact persisted request.
$retry=new TestOrder(107);$GLOBALS['orders'][107]=$retry;$GLOBALS['responses'][]=new WP_Error('timeout','timeout');$g->process_payment(107);
$first_body=end($GLOBALS['requests'])[1]['body'];
$safe=new WC_Gateway_MochiPay();
$GLOBALS['responses'][]=array('code'=>404,'body'=>json_encode(array('success'=>false,'message'=>'ORDER_NOT_FOUND','recovery_contract'=>'merchant-reference-v1')));
$reply=$response;$reply['merchant_order_id']=str_replace('-101','-107',$ref);$reply['order_id']=str_repeat('c',32);
$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($reply));
check($safe->process_payment(107)['result']==='success','verified server can replay saved request after conclusive not-found');
check(end($GLOBALS['requests'])[1]['body']===$first_body,'replay uses unchanged request ID and payload bytes');
$legacy=new TestOrder(108);$legacy->meta=array('_mochipay_create_uncertain'=>'yes','_mochipay_payment_method'=>'USDT_TRC20');$GLOBALS['orders'][108]=$legacy;
$GLOBALS['responses'][]=array('code'=>404,'body'=>json_encode(array('success'=>false,'message'=>'ORDER_NOT_FOUND')));$count=count($GLOBALS['requests']);$safe->process_payment(108);
check(count($GLOBALS['requests'])===$count+1,'legacy attempt never given a newly invented replay ID');
$unknown=new TestOrder(109);$GLOBALS['orders'][109]=$unknown;$GLOBALS['responses'][]=new WP_Error('timeout','timeout');$safe->process_payment(109);
$GLOBALS['responses'][]=array('code'=>500,'body'=>json_encode(array('success'=>false,'message'=>'SYSTEM_ERROR_LOAD_ORDER')));$count=count($GLOBALS['requests']);$safe->process_payment(109);
check(count($GLOBALS['requests'])===$count+1,'query failure cannot trigger replay');
$definite=new TestOrder(110);$GLOBALS['orders'][110]=$definite;$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode(array('success'=>false,'message'=>'INVALID_API_KEY')));$g->process_payment(110);
check(!$definite->get_meta('_mochipay_create_uncertain'),'explicit JSON validation rejection clears uncertain marker');
$conflict=new TestOrder(111);$GLOBALS['orders'][111]=$conflict;$GLOBALS['responses'][]=array('code'=>409,'body'=>json_encode(array('success'=>false,'message'=>'REQUEST_IN_PROGRESS')));$g->process_payment(111);
check($conflict->get_meta('_mochipay_create_uncertain')==='yes','in-progress conflict remains unresolved');
// Diagnostics must expose safe codes only and preserve legacy attempts.
$diag=new TestOrder(104);$GLOBALS['orders'][104]=$diag;
$GLOBALS['responses'][]=array('code'=>500,'body'=>json_encode(array('success'=>false,'message'=>'SYSTEM_ERROR_LOAD_ORDER')));
check($g->process_payment(104)['result']==='failure','server error remains unresolved');
check(strpos(end($diag->notes),'HTTP 500; SYSTEM_ERROR_LOAD_ORDER')!==false,'create diagnostic gives safe machine code');
check($diag->get_meta('_mochipay_create_uncertain')==='yes','server error retains uncertain attempt');
$GLOBALS['responses'][]=array('code'=>404,'body'=>json_encode(array('success'=>false,'message'=>'ORDER_NOT_FOUND')));
$count=count($GLOBALS['requests']);$g->process_payment(104);
check(count($GLOBALS['requests'])===$count+1 && end($GLOBALS['requests'])[1]['method']==='GET','legacy 404 cannot trigger duplicate create');
check(strpos(end($diag->notes),'HTTP 404; ORDER_NOT_FOUND')!==false,'recovery diagnostic separates not-found');
$notes=count($diag->notes);$GLOBALS['responses'][]=array('code'=>404,'body'=>json_encode(array('success'=>false,'message'=>'ORDER_NOT_FOUND')));$g->process_payment(104);
check(count($diag->notes)===$notes,'repeated failure does not spam order notes');
$private=new TestOrder(105);$GLOBALS['orders'][105]=$private;
$GLOBALS['responses'][]=new WP_Error('http_request_failed','private@email.test key=SECRET');$g->process_payment(105);
check(strpos(end($private->notes),'SECRET')===false && strpos(end($private->notes),'@')===false,'private upstream text excluded from diagnostic note');
$bad=new TestOrder(106);$GLOBALS['orders'][106]=$bad;$GLOBALS['responses'][]=array('code'=>401,'body'=>json_encode(array('success'=>false,'message'=>'INVALID_API_KEY')));$g->process_payment(106);
check(!$bad->get_meta('_mochipay_create_uncertain'),'definitive 401 clears uncertain flag');
$diag->meta['_mochipay_merchant_order_id']='WC-12345678-104';$method=new ReflectionMethod(WC_Gateway_MochiPay::class,'merchant_order_id');$method->setAccessible(true);
check($method->invoke($g,$diag)==='WC-12345678-104','saved merchant reference retained across store URL changes');
// Legacy Woo account payment can recover automatically with the advertised API contract.
$GLOBALS['responses'][]=array('code'=>404,'body'=>json_encode(array('success'=>false,'message'=>'ORDER_NOT_FOUND','recovery_contract'=>'merchant-reference-v1')));
$old_reply=$response;$old_reply['merchant_order_id']=str_replace('-101','-108',$ref);$old_reply['order_id']=str_repeat('d',32);
$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($old_reply));
check($safe->process_payment(108)['result']==='success','legacy account order resumes on reference-safe server');
check(!empty($legacy->meta['_mochipay_request_id']) && !$legacy->get_meta('_mochipay_create_uncertain'),'legacy recovery persists new stable request then clears unresolved flag');
// A successful unpaid order does not poison the next purchase of the same amount.
foreach(array(201,202) as $oid){$oNew=new TestOrder($oid);$GLOBALS['orders'][$oid]=$oNew;$invoice=$response;$invoice['order_id']=str_pad(dechex($oid),32,'0',STR_PAD_LEFT);$invoice['merchant_order_id']=str_replace('-101','-'.$oid,$ref);$GLOBALS['responses'][]=array('code'=>200,'body'=>json_encode($invoice));check($g->process_payment($oid)['result']==='success','distinct unpaid purchase '.$oid.' can create payment');}
check($GLOBALS['orders'][201]->meta['_mochipay_request_id']!==$GLOBALS['orders'][202]->meta['_mochipay_request_id'],'same amount purchases use independent request IDs');
check($GLOBALS['orders'][201]->meta['_mochipay_merchant_order_id']!==$GLOBALS['orders'][202]->meta['_mochipay_merchant_order_id'],'same amount purchases use independent merchant references');
echo "\n$checks checks passed (mock WordPress/WooCommerce/API).\n";
