"""Controlled transparent-wallet experiments. No peers, mainnet or real funds.
Run: python test-wallet-payments.py /absolute/path/to/litecoind /new/output/dir
Requires isolated_rpc.py in the same directory; standard library only.
"""
from pathlib import Path
import csv,datetime,decimal,hashlib,json,sys,tempfile
from isolated_rpc import Node
D=decimal.Decimal
SCALE=100000000
def units(value):
    result=D(str(value))*SCALE
    assert result==result.to_integral_value(), 'Non-integer litoshi amount'
    return int(result)
def rpc_amount(value):return float(D(value)/SCALE)
def main():
    daemon=Path(sys.argv[1]).resolve();out=Path(sys.argv[2]).resolve()
    out.mkdir(parents=True,exist_ok=True);checks=[];states=[];payments=[];n=None
    def check(label,condition):
        if not condition:raise AssertionError(label)
        checks.append({'name':label,'passed':True})
    def attempt(call):
        try:return {'ok':True,'result':call()}
        except RuntimeError as error:return {'ok':False,'error':str(error)}
    with tempfile.TemporaryDirectory(prefix='lw-wallet-payment-regtest-') as temporary:
        root=Path(temporary).resolve()
        check('Fresh directory under system temporary directory',root.parent==Path(tempfile.gettempdir()).resolve())
        try:
            n=Node(daemon,'wallet-payments',root)
            check('Regtest network',n.rpc('getblockchaininfo')['chain']=='regtest')
            check('No peers',n.rpc('getconnectioncount')==0)
            version=n.rpc('getnetworkinfo')['subversion']
            check('Pinned Core version','0.21.5.8' in version)
            for wallet in ['miner','immature','confirmed','pending','locked','encrypted','recipient','fees1','fees3','fees10']:
                n.rpc('createwallet',wallet)
            n.rpc('createwallet','watch',True,True)
            mine=n.rpc('getnewaddress','','bech32',wallet='miner')
            imm=n.rpc('getnewaddress','','bech32',wallet='immature')
            destination=n.rpc('getnewaddress','','bech32',wallet='recipient')
            def snapshot(name,wallet):
                balances=n.rpc('getbalances',wallet=wallet)
                coins=n.rpc('listunspent',0,9999999,[],True,wallet=wallet)
                row={'case':name,'wallet':wallet,'height':n.rpc('getblockcount'),
                     'balances':balances,'getbalanceLitoshi':units(n.rpc('getbalance',wallet=wallet)),
                     'listedUnspent':coins,'walletInfo':n.rpc('getwalletinfo',wallet=wallet)}
                states.append(row);return row
            def auto_candidate(wallet):
                raw=n.rpc('createrawtransaction',[],{destination:.1})
                return n.rpc('fundrawtransaction',raw,{'feeRate':.00002},wallet=wallet)
            n.rpc('generatetoaddress',1,imm)
            s=snapshot('coinbase_one_confirmation','immature')
            check('New coinbase is immature',units(s['balances']['mine']['immature'])==50*SCALE and s['getbalanceLitoshi']==0)
            s['construction']=attempt(lambda:auto_candidate('immature'))
            check('Immature coinbase cannot fund payment',not s['construction']['ok'])
            n.rpc('generatetoaddress',99,mine)
            s=snapshot('coinbase_100_confirmations','immature')
            check('At 100 confirmations wallet still reports immature',units(s['balances']['mine']['immature'])==50*SCALE and s['getbalanceLitoshi']==0)
            n.rpc('generatetoaddress',1,mine)
            s=snapshot('coinbase_101_confirmations','immature')
            check('At 101 confirmations wallet reports spendable reward',s['getbalanceLitoshi']==50*SCALE and units(s['balances']['mine']['immature'])==0)
            s['construction']=attempt(lambda:auto_candidate('immature'))
            check('Mature coinbase funds candidate',s['construction']['ok'])
            n.rpc('generatetoaddress',2,mine)
            addresses={w:n.rpc('getnewaddress','','bech32',wallet=w) for w in ['confirmed','locked','encrypted']}
            n.rpc('importaddress',addresses['confirmed'],'test watch-only address',False,False,wallet='watch')
            fee_addresses={}
            funding={a:1 for a in addresses.values()}
            for count in [1,3,10]:
                amounts=[SCALE//count]*count;amounts[-1]+=SCALE-sum(amounts)
                fee_addresses[count]=[]
                for amount in amounts:
                    address=n.rpc('getnewaddress','','bech32',wallet='fees'+str(count))
                    funding[address]=rpc_amount(amount);fee_addresses[count].append(address)
            funding_tx=n.rpc('sendmany','',funding,wallet='miner')
            n.rpc('generatetoaddress',1,mine)
            s=snapshot('confirmed_owned_output','confirmed');funded=auto_candidate('confirmed')
            signed=n.rpc('signrawtransactionwithwallet',funded['hex'],wallet='confirmed')
            s['completeSignature']=signed['complete'];s['localPolicy']=n.rpc('testmempoolaccept',[signed['hex']])[0]
            check('Confirmed owned output signs and meets local policy',signed['complete'] and s['localPolicy']['allowed'])
            s=snapshot('watch_only_same_output','watch')
            check('Watch-only sees same confirmed value',units(s['balances']['watchonly']['trusted'])==SCALE and units(s['balances']['mine']['trusted'])==0 and s['getbalanceLitoshi']==SCALE)
            s['signatureAttempt']=attempt(lambda:n.rpc('signrawtransactionwithwallet',funded['hex'],wallet='watch'))
            check('Watch-only cannot produce complete signature',not s['signatureAttempt']['ok'] or not s['signatureAttempt']['result']['complete'])
            coin=n.rpc('listunspent',1,9999999,[],True,wallet='locked')[0]
            n.rpc('lockunspent',False,[{'txid':coin['txid'],'vout':coin['vout']}],wallet='locked')
            s=snapshot('confirmed_output_selection_locked','locked');s['lockedOutpoints']=n.rpc('listlockunspent',wallet='locked')
            s['construction']=attempt(lambda:auto_candidate('locked'))
            check('Selection lock keeps displayed balance',s['getbalanceLitoshi']==SCALE and units(s['balances']['mine']['trusted'])==SCALE)
            check('Selection lock removes automatic candidates',not s['listedUnspent'] and not s['construction']['ok'])
            n.rpc('lockunspent',True,[{'txid':coin['txid'],'vout':coin['vout']}],wallet='locked')
            s=snapshot('same_output_selection_unlocked','locked');s['construction']=attempt(lambda:auto_candidate('locked'))
            check('Unlocking selection restores candidate construction',s['construction']['ok'])
            pending_address=n.rpc('getnewaddress','','bech32',wallet='pending')
            pending_tx=n.rpc('sendtoaddress',pending_address,1,wallet='miner')
            s=snapshot('external_receipt_zero_confirmations','pending');s['construction']=attempt(lambda:auto_candidate('pending'))
            check('Incoming unconfirmed value is separately reported',units(s['balances']['mine']['untrusted_pending'])==SCALE and s['getbalanceLitoshi']==0)
            check('Unsafe incoming output excluded from automatic selection',not s['construction']['ok'] and not s['listedUnspent'][0]['safe'])
            n.rpc('generatetoaddress',1,mine)
            s=snapshot('same_external_receipt_one_confirmation','pending');s['construction']=attempt(lambda:auto_candidate('pending'))
            check('Confirmation makes incoming value trusted in this test',s['getbalanceLitoshi']==SCALE and s['construction']['ok'])
            n.rpc('encryptwallet','public regtest-only passphrase',wallet='encrypted')
            s=snapshot('encrypted_wallet_authorization_locked','encrypted');funded_encrypted=auto_candidate('encrypted')
            s['signatureAttempt']=attempt(lambda:n.rpc('signrawtransactionwithwallet',funded_encrypted['hex'],wallet='encrypted'))
            check('Encrypted locked wallet still has confirmed balance',s['getbalanceLitoshi']==SCALE)
            check('Encrypted lock blocks signing rather than balance reading',not s['signatureAttempt']['ok'] and 'walletpassphrase' in s['signatureAttempt']['error'])
            n.rpc('walletpassphrase','public regtest-only passphrase',60,wallet='encrypted')
            s=snapshot('encrypted_wallet_authorization_unlocked','encrypted')
            unlocked_signed=n.rpc('signrawtransactionwithwallet',funded_encrypted['hex'],wallet='encrypted')
            s['completeSignature']=unlocked_signed['complete'];check('Documented local unlock permits signing',unlocked_signed['complete'])
            n.rpc('walletlock',wallet='encrypted')
            # All constructions below reuse unspent test outputs. Only the final sweep is broadcast.
            broadcast=None
            for count in [1,3,10]:
                wallet='fees'+str(count);coins=n.rpc('listunspent',1,9999999,[],True,wallet=wallet)
                check('Input budget is exactly 1 LTC for '+wallet,sum(units(c['amount']) for c in coins)==SCALE and len(coins)==count)
                inputs=[{'txid':c['txid'],'vout':c['vout']} for c in coins]
                for rate in [2,10]:
                    for mode in ['exact_full_balance','exact_0.4','subtract_from_0.4','send_max']:
                        amount=SCALE if mode in ['exact_full_balance','send_max'] else 40000000
                        raw=n.rpc('createrawtransaction',inputs,{destination:rpc_amount(amount)})
                        options={'feeRate':float(D(rate)/100000),'add_inputs':False,'changePosition':1}
                        if mode in ['subtract_from_0.4','send_max']:options['subtractFeeFromOutputs']=[0]
                        row={'case':mode,'input_count':count,'rate_litoshi_per_vbyte':rate,'input_litoshi':SCALE,'entered_litoshi':amount}
                        try:funded=n.rpc('fundrawtransaction',raw,options,wallet=wallet)
                        except RuntimeError as error:
                            row['error']=str(error);row['constructed']=False
                            check('Only full exact payment is rejected '+str((count,rate,mode)),mode=='exact_full_balance' and 'Insufficient funds' in row['error'])
                            payments.append(row);continue
                        check('Exact full balance is never silently reduced',mode!='exact_full_balance')
                        signed=n.rpc('signrawtransactionwithwallet',funded['hex'],wallet=wallet)
                        decoded=n.rpc('decoderawtransaction',signed['hex']);policy=n.rpc('testmempoolaccept',[signed['hex']])[0]
                        fee=SCALE-sum(units(o['value']) for o in decoded['vout'])
                        recipient=units(decoded['vout'][0]['value']);change=sum(units(o['value']) for o in decoded['vout'][1:])
                        row.update(constructed=True,recipient_litoshi=recipient,change_litoshi=change,fee_litoshi=fee,
                                   wallet_reduction_litoshi=SCALE-change,vsize=decoded['vsize'],weight=decoded['weight'],
                                   complete_signature=signed['complete'],local_policy=policy,changepos=funded['changepos'],txid=decoded['txid'],
                                   signed_hex=signed['hex'],selected_inputs=[{'txid':c['txid'],'vout':c['vout'],'value_litoshi':units(c['amount'])} for c in coins],
                                   decoded_outputs=[{'vout':o['n'],'value_litoshi':units(o['value']),'script_hex':o['scriptPubKey']['hex']} for o in decoded['vout']])
                        check('Signed candidate meets regtest policy '+str((count,rate,mode)),signed['complete'] and policy['allowed'])
                        check('Fee equals actual signed input-minus-output total '+str((count,rate,mode)),fee==units(funded['fee']) and SCALE==recipient+change+fee)
                        if mode=='exact_0.4':check('Exact recipient amount '+str((count,rate)),recipient==40000000 and SCALE-change==40000000+fee)
                        if mode=='subtract_from_0.4':check('Fee subtraction preserves total debit '+str((count,rate)),recipient==40000000-fee and change==60000000)
                        if mode=='send_max':
                            check('Sweep leaves no change '+str((count,rate)),change==0 and recipient==SCALE-fee and funded['changepos']==-1)
                            if count==1 and rate==2:broadcast=(wallet,signed['hex'],row)
                        payments.append(row)
            wallet,hex_tx,row=broadcast
            n.rpc('sendrawtransaction',hex_tx);n.rpc('generatetoaddress',1,mine)
            sweep={'txid':row['txid'],'senderBalanceLitoshi':units(n.rpc('getbalance',wallet=wallet)),
                   'recipientConfirmedLitoshi':units(n.rpc('getreceivedbyaddress',destination,1,wallet='recipient'))}
            check('One selected sweep actually mined and emptied its test wallet',sweep['senderBalanceLitoshi']==0 and sweep['recipientConfirmedLitoshi']==row['recipient_litoshi'])
            result={'schema':1,'tested_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
                    'network':'regtest','version':version,'binary_sha256':hashlib.sha256(daemon.read_bytes()).hexdigest(),
                    'peer_count':n.rpc('getconnectioncount'),'funding_txid':funding_tx,'pending_txid':pending_tx,
                    'states':states,'payments':payments,'mined_sweep':sweep,'checks':checks,
                    'scope':'One Core node, transparent P2WPKH outputs, RPC workflows, synthetic regtest LTC. 18 signed payment candidates and six rejected constructions; one selected sweep mined. No mainnet fee recommendation, GUI/mobile/hardware test, MWEB, exchange account or spend timing benchmark.'}
            (out/'results.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
            with (out/'payment-cases.csv').open('w',newline='',encoding='utf-8') as f:
                fields=['case','input_count','rate_litoshi_per_vbyte','input_litoshi','entered_litoshi','constructed','recipient_litoshi','change_litoshi','fee_litoshi','wallet_reduction_litoshi','vsize','weight','complete_signature','changepos','txid','error']
                w=csv.DictWriter(f,fieldnames=fields,extrasaction='ignore');w.writeheader();w.writerows(payments)
            with (out/'balance-states.csv').open('w',newline='',encoding='utf-8') as f:
                w=csv.writer(f);w.writerow(['case','height','mine_trusted_litoshi','mine_pending_litoshi','mine_immature_litoshi','watchonly_trusted_litoshi','getbalance_litoshi','listed_unspent_count'])
                for s in states:
                    mine_b=s['balances']['mine'];watch=s['balances'].get('watchonly',{})
                    w.writerow([s['case'],s['height'],units(mine_b['trusted']),units(mine_b['untrusted_pending']),units(mine_b['immature']),units(watch.get('trusted',0)),s['getbalanceLitoshi'],len(s['listedUnspent'])])
            print(json.dumps({'checks':len(checks),'states':len(states),'payments':len(payments),'mined_sweep':sweep,'version':version}))
        finally:
            if n:n.stop()
if __name__=='__main__':main()
