<?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 $m,$d;function __construct($c,$m,$d=null){$this->m=$m;$this->d=$d;}function get_error_message(){return $this->m;}function get_error_data(){return $this->d;}}
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');
echo "\n$checks checks passed (mock WordPress/WooCommerce/API).\n";
