"""Standard-library isolated Litecoin Core experiment. No mainnet funds or peers.
Run: python test-core.py /path/to/litecoind /path/to/new-output-directory
Requires isolated_rpc.py alongside this file. Never use an existing node directory.
"""
from pathlib import Path
import json,decimal,hashlib,tempfile,sys,datetime,csv
from isolated_rpc import Node
D=decimal.Decimal
def litoshi(v):return int(D(str(v))*100000000)

def main():
 daemon=Path(sys.argv[1]).resolve();out=Path(sys.argv[2]).resolve();out.mkdir(parents=True,exist_ok=True)
 checks=[];rows=[];n=None
 def check(name,value):
  assert value,name
  checks.append({'test':name,'passed':True})
 with tempfile.TemporaryDirectory(prefix='lw-beginner-regtest-') as temporary:
  root=Path(temporary).resolve()
  check('Fresh test directory under system temporary directory',root.parent==Path(tempfile.gettempdir()).resolve())
  try:
   n=Node(daemon,'beginner',root)
   check('Isolated regtest, no network peers',n.rpc('getblockchaininfo')['chain']=='regtest' and n.rpc('getconnectioncount')==0)
   version=n.rpc('getnetworkinfo')['subversion'];check('Pinned Core version','0.21.5.8' in version)
   for w in ['miner','recipient','sender']:n.rpc('createwallet',w)
   mine=n.rpc('getnewaddress','','bech32',wallet='miner');n.rpc('generatetoaddress',101,mine)
   old=n.rpc('getnewaddress','first','bech32',wallet='recipient')
   new=n.rpc('getnewaddress','second','bech32',wallet='recipient')
   funding=n.rpc('sendmany','',{old:.1,new:.2},wallet='miner');n.rpc('generatetoaddress',1,mine)
   first=n.rpc('getreceivedbyaddress',old,1,wallet='recipient')
   more=[n.rpc('getnewaddress','','bech32',wallet='recipient') for _ in range(40)]
   reuse=n.rpc('sendtoaddress',old,.3,wallet='miner');n.rpc('generatetoaddress',1,mine)
   old_total=n.rpc('getreceivedbyaddress',old,1,wallet='recipient');new_total=n.rpc('getreceivedbyaddress',new,1,wallet='recipient');balance=n.rpc('getbalance',wallet='recipient')
   check('Old address still belongs to wallet',n.rpc('getaddressinfo',old,wallet='recipient')['ismine'])
   check('Old address received 0.1 then another 0.3 LTC',litoshi(first)==10000000 and litoshi(old_total)==40000000)
   check('New address received 0.2 LTC',litoshi(new_total)==20000000)
   check('One wallet total includes both addresses',litoshi(balance)==60000000)
   addresses={'oldAddress':old,'newAddress':new,'additionalAddressesGenerated':len(more),'firstOldReceiptLTC':first,'oldConfirmedTotalLTC':old_total,'newConfirmedTotalLTC':new_total,'walletConfirmedBalanceLTC':balance,'fundingTxid':funding,'reuseTxid':reuse}
   a1=n.rpc('getnewaddress','','bech32',wallet='sender');a2=n.rpc('getnewaddress','','bech32',wallet='sender')
   fund=n.rpc('sendmany','',{a1:.5,a2:.5},wallet='miner');n.rpc('generatetoaddress',1,mine)
   coins=n.rpc('listunspent',1,9999999,[],True,wallet='sender');coins.sort(key=lambda x:x['vout']);check('Two explicit confirmed inputs',len(coins)==2)
   receive=n.rpc('getnewaddress','','bech32',wallet='recipient');own=n.rpc('getnewaddress','','bech32',wallet='sender')
   def candidate(coin,outputs,sequence):
    raw=n.rpc('createrawtransaction',[{'txid':coin['txid'],'vout':coin['vout'],'sequence':sequence}],outputs)
    signed=n.rpc('signrawtransactionwithwallet',raw,[{'txid':coin['txid'],'vout':coin['vout'],'scriptPubKey':coin['scriptPubKey'],'amount':coin['amount']}],wallet='sender')
    check('Complete test signature',signed['complete']);dec=n.rpc('decoderawtransaction',signed['hex'])
    fee=litoshi(coin['amount'])-sum(litoshi(o['value']) for o in dec['vout'])
    return {'hex':signed['hex'],'txid':dec['txid'],'vsize':dec['vsize'],'feeLitoshi':fee,'sequence':sequence,'outputs':[{ 'address':v['scriptPubKey'].get('addresses',[''])[0], 'valueLitoshi':litoshi(v['value'])} for v in dec['vout']]}
   original=candidate(coins[0],{receive:.1,own:.39999},4294967293)
   check('Signed-only payment absent from mempool',original['txid'] not in n.rpc('getrawmempool'))
   check('Initial candidate meets local policy',n.rpc('testmempoolaccept',[original['hex']])[0]['allowed'])
   n.rpc('sendrawtransaction',original['hex']);check('Original payment is in local mempool',original['txid'] in n.rpc('getrawmempool'))
   low=candidate(coins[0],{own:.499995},4294967293);low_test=n.rpc('testmempoolaccept',[low['hex']])[0]
   check('Lower total fee replacement rejected',not low_test['allowed'])
   high=candidate(coins[0],{own:.49997},4294967293);high_test=n.rpc('testmempoolaccept',[high['hex']])[0]
   print(json.dumps({'original':original,'lowPolicy':low_test,'highPolicy':high_test}),flush=True)
   check('Default policy rejects higher fee replacement',not high_test['allowed'] and high_test.get('reject-reason')=='txn-mempool-conflict')
   default_tests={'lowFee':low_test,'highFee':high_test}
   n.stop();n.start(replacement=True)
   for w in ['miner','recipient','sender']:n.rpc('loadwallet',w)
   check('Original survived restart',original['txid'] in n.rpc('getrawmempool'))
   low_test=n.rpc('testmempoolaccept',[low['hex']])[0];high_test=n.rpc('testmempoolaccept',[high['hex']])[0]
   check('Enabled policy rejects lower total fee',not low_test['allowed'])
   check('Enabled policy accepts higher fee replacement',high_test['allowed'])
   n.rpc('sendrawtransaction',high['hex']);pool=n.rpc('getrawmempool')
   check('Replacement displaces original locally',high['txid'] in pool and original['txid'] not in pool)
   n.rpc('generatetoaddress',1,mine)
   check('Replacement confirmed',n.rpc('gettransaction',high['txid'],wallet='sender')['confirmations']==1)
   check('Original intended receiver got no funds from replaced payment',litoshi(n.rpc('getreceivedbyaddress',receive,1,wallet='recipient'))==0)
   mined=candidate(coins[1],{receive:.1,own:.39999},4294967295);n.rpc('sendrawtransaction',mined['hex']);n.rpc('generatetoaddress',1,mine)
   conflict=candidate(coins[1],{own:.4999},4294967293);conflict_test=n.rpc('testmempoolaccept',[conflict['hex']])[0]
   check('Conflicting spend after confirmation rejected',not conflict_test['allowed'])
   check('Confirmed recipient received 0.1 LTC',litoshi(n.rpc('getreceivedbyaddress',receive,1,wallet='recipient'))==10000000)
   cancellation={'signedNotBroadcast':{'mempoolPresent':False,'feeLitoshi':original['feeLitoshi']},'original':original,'lowFeeReplacement':{**low,'localPolicy':low_test},'successfulReplacement':{**high,'localPolicy':high_test},'confirmedPayment':mined,'postConfirmationConflict':{**conflict,'localPolicy':conflict_test},'limits':'One isolated Core node, transparent P2WPKH outputs; no peers, mainnet broadcasts, propagation measurement, race probability, exchange account or wallet GUI test.'}
   cancellation['defaultPolicy']=default_tests
   cancellation['successfulReplacement']['nodeFlag']='-mempoolreplacement=1'
   result={'schema':1,'testedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'network':'regtest','version':version,'binarySha256':hashlib.sha256(daemon.read_bytes()).hexdigest(),'addressReuse':addresses,'cancellation':cancellation,'checks':checks}
   (out/'core-results.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
   with (out/'cancellation-cases.csv').open('w',newline='',encoding='utf-8') as f:
    w=csv.writer(f);w.writerow(['case','fee_litoshi','vsize','local_outcome'])
    w.writerows([['signed_not_broadcast',original['feeLitoshi'],original['vsize'],'not in mempool'],['original_broadcast',original['feeLitoshi'],original['vsize'],'in local mempool'],['default_policy_lower_fee',low['feeLitoshi'],low['vsize'],default_tests['lowFee'].get('reject-reason')],['default_policy_higher_fee',high['feeLitoshi'],high['vsize'],default_tests['highFee'].get('reject-reason')],['enabled_lower_fee',low['feeLitoshi'],low['vsize'],low_test.get('reject-reason')],['enabled_higher_fee',high['feeLitoshi'],high['vsize'],'replaced original and confirmed'],['confirmed_original',mined['feeLitoshi'],mined['vsize'],'recipient received 0.1'],['conflict_after_confirmation',conflict['feeLitoshi'],conflict['vsize'],conflict_test.get('reject-reason')]])
   print(json.dumps({'version':version,'checks':len(checks),'addressReuse':addresses,'lowReject':low_test,'highAccepted':high_test,'confirmedConflict':conflict_test}))
  finally:
   if n:n.stop()
if __name__=='__main__':main()
