"""Synthetic address fixtures checked by isolated Core main/regtest parsers.
No wallets, peers, broadcasts, deposits or mainnet chain observations.
Run: python address-checks.py /absolute/path/to/litecoind /new/output/directory
All generated addresses are fixtures. NEVER SEND FUNDS TO THEM.
"""
from pathlib import Path
import base64,csv,datetime,hashlib,json,socket,subprocess,sys,tempfile,time,urllib.request,urllib.error
ALPHABET='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
CHARSET='qpzry9x8gf2tvdw0s3jn54khce6mua7l'
def base58(version,payload):
    raw=bytes([version])+payload;raw+=hashlib.sha256(hashlib.sha256(raw).digest()).digest()[:4]
    num=int.from_bytes(raw,'big');encoded=''
    while num:num,rem=divmod(num,58);encoded=ALPHABET[rem]+encoded
    return '1'*(len(raw)-len(raw.lstrip(b'\0')))+encoded
def bech32(hrp,program):
    values=[0];acc=bits=0
    for byte in program:
        acc=(acc<<8)|byte;bits+=8
        while bits>=5:bits-=5;values.append((acc>>bits)&31)
    if bits:values.append((acc<<(5-bits))&31)
    expanded=[ord(c)>>5 for c in hrp]+[0]+[ord(c)&31 for c in hrp]
    chk=1
    for value in expanded+values+[0]*6:
        top=chk>>25;chk=((chk&0x1ffffff)<<5)^value
        for index,g in enumerate([0x3b6a57b2,0x26508e6d,0x1ea119fa,0x3d4233dd,0x2a1462b3]):
            if (top>>index)&1:chk^=g
    chk^=1;checksum=[(chk>>5*(5-i))&31 for i in range(6)]
    return hrp+'1'+''.join(CHARSET[v] for v in values+checksum)
def fixtures():
    a=hashlib.sha256(b'Litecoin.watch synthetic address fixture A 2026-10-03').digest()[:20]
    b=hashlib.sha256(b'Litecoin.watch synthetic address fixture B 2026-10-03').digest()[:20]
    l=base58(48,a);native=bech32('ltc',a)
    rows=[('ltc_legacy_L','LTC mainnet P2PKH',l,True,False),('ltc_script_M','LTC mainnet modern P2SH',base58(50,a),True,False),
          ('shared_script_3','Historical P2SH version 5',base58(5,a),True,False),('bitcoin_legacy_1','Bitcoin mainnet P2PKH version 0',base58(0,a),False,False),
          ('shared_test_legacy','Test-network P2PKH version 111',base58(111,a),False,True),('shared_test_script','Test-network P2SH version 196',base58(196,a),False,True),
          ('ltc_test_script','Litecoin test P2SH version 58',base58(58,a),False,True),('ltc_native','LTC mainnet witness v0',native,True,False),
          ('bitcoin_native','Bitcoin witness-v0 bc HRP',bech32('bc',a),False,False),('ltc_test_native','Litecoin testnet tltc HRP',bech32('tltc',a),False,False),
          ('ltc_regtest_native','Litecoin regtest rltc HRP',bech32('rltc',a),False,True),('other_valid_ltc','Different valid LTC witness-v0 payload',bech32('ltc',b),True,False),
          ('uppercase_native','All-uppercase LTC Bech32',native.upper(),True,False),('mixed_case_native','Mixed-case LTC Bech32',native[:3].upper()+native[3:],False,False),
          ('base58_typo','One checksum-character substitution',l[:-1]+('1' if l[-1]!='1' else '2'),False,False),
          ('bech32_typo','One Bech32 checksum-character substitution',native[:-1]+('q' if native[-1]!='q' else 'p'),False,False),
          ('evm_style','EVM-style 0x string', '0x'+a.hex(),False,False),('payment_uri','URI rather than bare address','litecoin:'+native+'?amount=1',False,False)]
    return [{'id':i,'description':d,'address':address,'expected_main':main,'expected_regtest':reg} for i,d,address,main,reg in rows]
class ParserNode:
    def __init__(self,daemon,root,chain):
        self.root=root/chain;self.root.mkdir();self.chain=chain
        with socket.socket() as s:s.bind(('127.0.0.1',0));self.port=s.getsockname()[1]
        self.cookie=self.root/('regtest/.cookie' if chain=='regtest' else '.cookie')
        args=[str(daemon),'-chain='+chain,'-datadir='+str(self.root),'-rpcport='+str(self.port),'-rpcbind=127.0.0.1',
              '-listen=0','-connect=0','-dnsseed=0','-discover=0','-upnp=0','-disablewallet=1','-dbcache=32','-printtoconsole=0']
        self.process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0))
        try:
            for _ in range(120):
                try:self.rpc('getblockchaininfo');break
                except Exception:
                    if self.process.poll() is not None:raise RuntimeError('Parser node exited')
                    time.sleep(.25)
            else:raise RuntimeError('Parser node startup timeout')
        except BaseException:
            self.process.terminate();self.process.wait(timeout=30);raise
    def rpc(self,method,*params):
        cookie=self.cookie.read_text().strip()
        req=urllib.request.Request('http://127.0.0.1:'+str(self.port)+'/',data=json.dumps({'jsonrpc':'1.0','id':'address-check','method':method,'params':params}).encode(),
                                  headers={'Authorization':'Basic '+base64.b64encode(cookie.encode()).decode(),'Content-Type':'application/json'})
        try:
            with urllib.request.urlopen(req,timeout=15) as r:obj=json.load(r)
        except urllib.error.HTTPError as err:obj=json.loads(err.read())
        if obj['error']:raise RuntimeError(method+': '+str(obj['error']))
        return obj['result']
    def stop(self):
        try:self.rpc('stop')
        except Exception:
            if self.process.poll() is None:self.process.terminate()
        self.process.wait(timeout=30)
def main():
    daemon=Path(sys.argv[1]).resolve();out=Path(sys.argv[2]).resolve();out.mkdir(parents=True,exist_ok=True)
    nodes=[];checks=[];results=[];metadata={}
    def check(name,value):
        assert value,name;checks.append({'name':name,'passed':True})
    with tempfile.TemporaryDirectory(prefix='lw-address-parser-') as tmp:
        root=Path(tmp).resolve();check('Fresh temporary test root',root.parent==Path(tempfile.gettempdir()).resolve())
        try:
            for chain in ['main','regtest']:
                n=ParserNode(daemon,root,chain);nodes.append(n);info=n.rpc('getblockchaininfo');version=n.rpc('getnetworkinfo')['subversion']
                peers=n.rpc('getconnectioncount');check(chain+' network mode',info['chain']==chain)
                check(chain+' no peers or synchronization',peers==0 and info['blocks']==0)
                check(chain+' pinned Core version','0.21.5.8' in version)
                metadata[chain]={'chain':chain,'height':info['blocks'],'peers':peers,'version':version,'wallet_disabled':True}
                for fixture in fixtures():
                    observed=n.rpc('validateaddress',fixture['address']);check(chain+'/'+fixture['id'],observed['isvalid']==fixture['expected_'+chain])
                    results.append({'network_mode':chain,**fixture,'observed':observed})
            main={r['id']:r['observed'] for r in results if r['network_mode']=='main'}
            check('Historical 3 and modern M encode same output script',main['shared_script_3']['scriptPubKey']==main['ltc_script_M']['scriptPubKey'])
            check('Uppercase Bech32 encodes same script',main['ltc_native']['scriptPubKey']==main['uppercase_native']['scriptPubKey'])
            check('Two distinct LTC destinations both pass',main['ltc_native']['isvalid'] and main['other_valid_ltc']['isvalid'] and main['ltc_native']['scriptPubKey']!=main['other_valid_ltc']['scriptPubKey'])
            result={'schema':1,'release':'2026-10-03-v1','tested_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
                    'binary_sha256':hashlib.sha256(daemon.read_bytes()).hexdigest(),'nodes':metadata,'fixture_count':18,'rpc_checks':36,
                    'scope':'Core address parsing on disconnected mainnet-mode and regtest nodes, both at height zero, wallets disabled. Synthetic hash payloads; no ownership, service-credit, recovery, wallet-GUI, MWEB or transfer test. NEVER SEND FUNDS TO ANY FIXTURE.',
                    'results':results,'assertions':checks}
            (out/'address-results.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
            with (out/'address-checks.csv').open('w',newline='',encoding='utf-8') as f:
                w=csv.writer(f);w.writerow(['network_mode','fixture_id','description','valid','script_hex','fixture_address_do_not_send'])
                for r in results:w.writerow([r['network_mode'],r['id'],r['description'],r['observed']['isvalid'],r['observed'].get('scriptPubKey',''),r['address']])
            print(json.dumps({'fixtures':18,'rpc_checks':36,'assertions':len(checks),'nodes':metadata}))
        finally:
            for n in reversed(nodes):n.stop()
if __name__=='__main__':main()
