# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Exporting of wallet files and similar things.
#
# Start simulator with:   simulator.py --eff --set nfc=1
#
import pytest, time, os, json, io, bech32
from bip32 import BIP32Node
from descriptor import Descriptor
from mnemonic import Mnemonic
from ckcc_protocol.constants import *
from helpers import xfp2str, slip132undo
from conftest import simulator_fixed_xfp, simulator_fixed_tprv, simulator_fixed_words, simulator_fixed_xprv
from pprint import pprint
from charcodes import KEY_NFC, KEY_QR


@pytest.fixture
def expect_acctnum_captured(settings_get, settings_set):
    # verify account number got captured, if non-zero; so address search covers it
    b4 = settings_get('accts', [])
    settings_set('accts', [])

    def doit(expect_acctnum):
        if isinstance(expect_acctnum, str):
            expect_acctnum = int(expect_acctnum)
        if not expect_acctnum:
            # we don't store zero
            return []

        aft = settings_get('accts', [])
        if not aft:
            time.sleep(.1)
            aft = settings_get('accts', [])
        types = [a for a,b in aft if b == expect_acctnum]
        assert len(types) >= 1, f'didnt capture account num {expect_acctnum}: {aft!r}'

        return types

    yield doit

    settings_set('accts', b4)

@pytest.mark.bitcoind
@pytest.mark.parametrize('acct_num', [None, '0', '99', '123'])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
def test_export_core(way, dev, use_regtest, acct_num, pick_menu_item, goto_home, cap_story,
                     need_keypress, microsd_path, virtdisk_path, bitcoind_wallet, bitcoind_d_wallet,
                     enter_number, nfc_read_text, load_export, bitcoind, press_select,
                     skip_if_useless_way, expect_acctnum_captured):

    skip_if_useless_way(way)

    # test UX and operation of the 'bitcoin core' wallet export
    use_regtest()
    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item('Bitcoin Core')

    time.sleep(0.1)
    title, story = cap_story()

    assert 'This saves' in story
    assert 'including the public keys' in story

    assert 'Press (1) to' in story
    if acct_num is not None:
        need_keypress('1')
        time.sleep(0.1)
        enter_number(acct_num)
    else:
        acct_num = '0'
        press_select()

    expect_acctnum_captured(acct_num)

    export = load_export(way, label="Bitcoin Core", is_json=False, addr_fmt=AF_P2WPKH)
    fp = io.StringIO(export).readlines()

    addrs = []
    imm_js = None
    imd_js = None
    imd_js_tr = None
    tr = False
    for ln in fp:
        if ln.startswith("p2tr:"):
            tr = True

        if 'importmulti' in ln:
            # PLAN: this will become obsolete
            assert ln.startswith("importmulti '")
            assert ln.endswith("'\n")
            assert not imm_js, "dup importmulti lines"
            imm_js = ln[13:-2]
        elif "importdescriptors '" in ln:
            ln = ln.strip()
            assert ln.startswith("importdescriptors '")
            if tr:
                imd_js_tr = ln[19:-1]
                tr = False
            else:
                imd_js = ln[19:-1]
        elif '=>' in ln:
            path, addr = ln.strip().split(' => ', 1)
            sk = BIP32Node.from_wallet_key(simulator_fixed_tprv).subkey_for_path(path)
            if path.startswith(f"m/86h/1h/{acct_num}h/0"):
                assert addr.startswith('bcrt1p')
                assert addr == sk.address(addr_fmt="p2tr", chain="XRT")
            else:
                assert path.startswith(f"m/84h/1h/{acct_num}h/0")
                assert addr.startswith("bcrt1q")
                assert addr == sk.address(addr_fmt="p2wpkh", chain="XRT")
            addrs.append(addr)

    assert len(addrs) == 6

    xfp = xfp2str(simulator_fixed_xfp).lower()

    if imm_js:
        obj = json.loads(imm_js)
        for n, here in enumerate(obj):
            assert here['range'] == [0, 1000]
            assert here['timestamp'] == 'now'
            assert here['internal'] == bool(n)
            assert here['keypool'] == True
            assert here['watchonly'] == True

            d = here['desc']
            desc, chk = d.split('#', 1)
            assert len(chk) == 8
            assert desc.startswith(f'wpkh([{xfp}/84h/1h/{acct_num}h]')

            expect = BIP32Node.from_wallet_key(simulator_fixed_tprv)\
                        .subkey_for_path(f"84'/1'/{acct_num}'").hwif()

            assert expect in desc
            assert expect+f'/{n}/*' in desc

        if bitcoind.has_bdb:
            # test against bitcoind
            # only legacy wallets do support importmulti
            for x in obj:
                x['label'] = 'testcase'

            bitcoind_wallet.importmulti(obj)
            x = bitcoind_wallet.getaddressinfo(addrs[-1])
            pprint(x)
            assert x['address'] == addrs[-1]
            # assert x['iswatchonly'] == True
            assert x['iswitness'] is True
            # assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1)

    # importdescriptors -- its better
    assert imd_js
    obj = json.loads(imd_js)
    for n, here in enumerate(obj):
        assert range not in here
        assert here['timestamp'] == 'now'
        assert here['internal'] == bool(n)

        d = here['desc']
        desc, chk = d.split('#', 1)
        assert len(chk) == 8
        assert desc.startswith(f'wpkh([{xfp}/84h/1h/{acct_num}h]')

        expect = BIP32Node.from_wallet_key(simulator_fixed_tprv)\
                    .subkey_for_path(f"84'/1'/{acct_num}'").hwif()

        assert expect in desc
        assert expect+f'/{n}/*' in desc

        res = bitcoind_d_wallet.importdescriptors(obj)
        assert res[0]["success"]
        assert res[1]["success"]
        x = bitcoind_d_wallet.getaddressinfo(addrs[2])
        pprint(x)
        assert x['address'] == addrs[2]
        assert x['iswatchonly'] == False
        assert x['iswitness'] == True
        assert x['solvable'] == True
        assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
        assert x['hdkeypath'].replace("'", "h") == f"m/84h/1h/{acct_num}h/0/%d" % 2

    assert imd_js_tr
    obj = json.loads(imd_js_tr)
    for n, here in enumerate(obj):
        assert here['timestamp'] == 'now'
        assert here['internal'] == bool(n)

        d = here['desc']
        desc, chk = d.split('#', 1)
        assert len(chk) == 8

        assert desc.startswith(f'tr([{xfp}/86h/1h/{acct_num}h]')

        expect = BIP32Node.from_wallet_key(simulator_fixed_tprv) \
            .subkey_for_path(f"m/86h/1h/{acct_num}h").hwif()

        assert expect in desc
        assert expect + f'/{n}/*' in desc

        # test against bitcoind -- needs a "descriptor native" wallet
        res = bitcoind_d_wallet.importdescriptors(obj)
        assert res[0]["success"]
        assert res[1]["success"]

        x = bitcoind_d_wallet.getaddressinfo(addrs[-1])
        pprint(x)
        assert x['address'] == addrs[-1]
        assert x['iswatchonly'] is False
        assert x['iswitness'] is True
        assert x['solvable'] is True
        assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower()
        assert x['hdkeypath'].replace("'", "h") == f"m/86h/1h/{acct_num}h/0/%d" % 2


@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
def test_export_wasabi(way, dev, pick_menu_item, goto_home, cap_story, press_select, microsd_path,
                       nfc_read_json, virtdisk_path, testnet, use_mainnet,
                        load_export, skip_if_useless_way):

    skip_if_useless_way(way)

    # test UX and operation of the 'wasabi wallet export'
    if not testnet:
        use_mainnet()

    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item('Wasabi Wallet')

    time.sleep(0.1)
    title, story = cap_story()

    assert 'This saves a skeleton Wasabi' in story
    press_select()

    obj = load_export(way, label="Wasabi wallet", is_json=True, addr_fmt=AF_P2WPKH)

    assert 'MasterFingerprint' in obj
    assert 'ExtPubKey' in obj
    assert 'ColdCardFirmwareVersion' in obj
    
    xpub = obj['ExtPubKey']
    assert xpub.startswith('xpub')

    assert obj['MasterFingerprint'] == xfp2str(simulator_fixed_xfp)

    got = BIP32Node.from_wallet_key(xpub)
    expect = BIP32Node.from_wallet_key(simulator_fixed_tprv).subkey_for_path(f"84'/{int(testnet)}'/0'")

    assert got.sec() == expect.sec()

        
@pytest.mark.parametrize('mode', [ "Classic P2PKH", "P2SH-Segwit", "Segwit P2WPKH"])
@pytest.mark.parametrize('acct_num', [ None, '9897'])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('name', ["Electrum"])#, "Blue"])  # same tests, not needed
def test_export_electrum(way, dev, mode, acct_num, pick_menu_item, goto_home, cap_story, need_keypress,
                         microsd_path, nfc_read_json, virtdisk_path, use_mainnet, testnet, load_export,
                         press_select, skip_if_useless_way, expect_acctnum_captured, name):
    # lightly test electrum wallet export
    skip_if_useless_way(way)

    if not testnet:
        use_mainnet()
    if "P2PKH" in mode:
        af = AF_CLASSIC
    elif "P2SH" in mode:
        af = AF_P2WPKH_P2SH
    else:
        af = AF_P2WPKH
    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item(f'{name} Wallet')

    time.sleep(0.1)
    title, story = cap_story()

    assert f'This saves a skeleton {name} wallet' in story

    assert 'Press (1) to' in story
    if acct_num is not None:
        need_keypress('1')
        time.sleep(0.1)
        for n in acct_num:
            need_keypress(n)

    press_select()

    time.sleep(0.1)
    pick_menu_item(mode)

    expect_acctnum_captured(acct_num)

    obj = load_export(way, label=f"{name} wallet", is_json=True, addr_fmt=af)

    ks = obj['keystore']
    assert ks['ckcc_xfp'] == simulator_fixed_xfp

    assert ks['hw_type'] == 'coldcard'
    assert ks['type'] == 'hardware'

    deriv = ks['derivation']
    assert deriv.startswith('m/')
    assert int(deriv.split("/")[1][:-1]) in {44, 84, 49}        # weak
    assert deriv.split("/")[3] == (acct_num or '0')+"h"

    xpub = ks['xpub']
    assert xpub[1:4] == 'pub'

    if xpub[0] in 'tx':
        if testnet:
            assert xpub[0] == "t"
        else:
            assert xpub[0] == "x"
        # no slip132 here

        got = BIP32Node.from_wallet_key(xpub)
        expect = BIP32Node.from_wallet_key(simulator_fixed_tprv).subkey_for_path(deriv)

        assert got.sec() == expect.sec()


@pytest.mark.parametrize('acct_num', [ None, '99', '1236'])
@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('netcode', ["XTN", "BTC"])
@pytest.mark.parametrize('app', [
    # no need to run them all - just name check differs
    ("Generic JSON", "Generic Export"),
    # ("Nunchuk", "Nunchuk Wallet"),
    # These differ only in the menu title. If that changes, add them back here... test latest only
    # ("Lily Wallet", "Lily Wallet"),
    # ("Sparrow Wallet", "Sparrow Wallet"),
    # ("Theya", "Theya Wallet"),
    ("Bitcoin Safe", "Bitcoin Safe Wallet"),
])
def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap_story, need_keypress,
                         microsd_path, nfc_read_json, virtdisk_path, addr_vs_path, enter_number,
                         load_export, netcode, use_mainnet, press_select,
                         skip_if_useless_way, expect_acctnum_captured):

    skip_if_useless_way(way)

    if netcode == "BTC":
        use_mainnet()

    export_mi, app_f_name = app
    # test UX and values produced.
    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item(export_mi)

    time.sleep(0.1)
    title, story = cap_story()
    assert 'JSON file' in story

    assert "Press (1)" in story
    if acct_num:
        need_keypress('1')
        time.sleep(0.1)
        enter_number(acct_num)
    else:
        acct_num = '0'
        press_select()

    expect_acctnum_captured(acct_num)
    obj = load_export(way, label=app_f_name, is_json=True, addr_fmt=AF_CLASSIC)

    for fn in ['xfp', 'xpub', 'chain']:
        assert fn in obj
        assert obj[fn]
    assert obj['account'] == int(acct_num or 0)

    for fn in ['bip44', 'bip49', 'bip84', 'bip48_1', 'bip48_2', 'bip45']:
        if obj['account'] and fn == 'bip45':
            assert fn not in obj
            continue

        assert fn in obj
        v = obj[fn]
        assert all([i in v for i in ['deriv', 'name', 'xpub', 'xfp']])

        if fn == 'bip45':
            assert v['deriv'] == "m/45h"
        elif 'bip48' not in fn:
            assert v['deriv'].endswith(f"h/{acct_num}h")
        else:
            b48n = fn[-1]
            assert v['deriv'].endswith(f"h/{acct_num}h/{b48n}h")

        node = BIP32Node.from_wallet_key(v['xpub'])
        assert v['xpub'] == node.hwif()
        first = node.subkey_for_path('0/0')
        addr = v.get('first', None)

        if fn == 'bip44':
            assert first.address(chain=netcode) == v['first']
            addr_vs_path(addr, v['deriv'] + '/0/0', AF_CLASSIC, chain=netcode)
        elif ('bip48_' in fn) or (fn == 'bip45'):
            # multisig: cant do addrs
            assert addr == None
        else:
            assert v['_pub'][1:4] == 'pub'
            assert slip132undo(v['_pub'])[0] == v['xpub']

            h20 = first.hash160()
            if fn == 'bip84':
                assert addr == bech32.encode(addr[0:2], 0, h20)
                addr_vs_path(addr, v['deriv'] + '/0/0', AF_P2WPKH, chain=netcode)
            elif fn == 'bip49':
                # don't have test logic for verifying these addrs
                # - need to make script, and bleh
                assert first.address(addr_fmt="p2sh-p2wpkh", chain=netcode) == v['first']
            else:
                assert False

@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('testnet', [True, False])
@pytest.mark.parametrize('acct_num', [None, '0', '99', '123'])
def test_export_unchained(way, dev, pick_menu_item, goto_home, cap_story, need_keypress, acct_num,
              microsd_path, nfc_read_json, virtdisk_path, testnet, enter_number,
              load_export, settings_set, use_mainnet, press_select,
              skip_if_useless_way, expect_acctnum_captured):

    # test UX and operation of the 'unchained export'
    skip_if_useless_way(way)

    if not testnet:
        use_mainnet()
    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item('Unchained')

    time.sleep(0.1)
    title, story = cap_story()

    assert 'Unchained' in story
    assert "Capital" not in story
    assert 'Press (1) to' in story
    if acct_num is not None:
        need_keypress('1')
        time.sleep(0.1)
        enter_number(acct_num)
    else:
        acct_num = '0'
        press_select()

    expect_acctnum_captured(acct_num)
    obj = load_export(way, label="Unchained", is_json=True)

    ek = simulator_fixed_tprv if testnet else simulator_fixed_xprv
    root = BIP32Node.from_wallet_key(ek)

    assert obj['xfp'] == xfp2str(simulator_fixed_xfp)
    assert obj['account'] == int(acct_num)
    if acct_num == "0":
        assert obj['p2sh_deriv'] == "m/45h"
        addr_formats = ['p2sh_p2wsh', 'p2sh', 'p2wsh']
    else:
        assert 'p2sh_deriv' not in obj
        addr_formats = ['p2sh_p2wsh', 'p2wsh']

    for k in addr_formats:
        xpub = slip132undo(obj[k])[0] if k != 'p2sh' else obj[k]
        node = BIP32Node.from_wallet_key(xpub)
        assert xpub == node.hwif()
        sk = root.subkey_for_path(obj[f'{k}_deriv'])
        #assert node.chain_code() == sk.chain_code()
        assert node.hwif() == sk.hwif()


@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize('netcode', ["BTC", "XTN"])
def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, microsd_path,
                           addr_vs_path, virtdisk_path, nfc_read_text, cap_story, use_mainnet,
                           load_export, netcode, skip_if_useless_way):
    # test UX and values produced.
    skip_if_useless_way(way)

    if netcode == "BTC":
        use_mainnet()

    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('File Management')
    pick_menu_item('Export Wallet')
    pick_menu_item('Dump Summary')

    time.sleep(0.1)
    title, story = cap_story()

    assert 'Saves a text file' in story
    press_select()

    contents = load_export(way, label="Summary", is_json=False, addr_fmt=AF_CLASSIC)
    fp = io.StringIO(contents).readlines()

    xfp = xfp2str(simulator_fixed_xfp).upper()

    ek = simulator_fixed_tprv if netcode == "XTN" else simulator_fixed_xprv
    root = BIP32Node.from_wallet_key(ek)

    for ln in fp:
        if 'fingerprint' in ln:
            assert ln.strip().endswith(xfp)

        if '=>' not in ln:
            continue

        lhs, rhs = ln.strip().split(' => ')
        assert lhs.startswith('m/')
        rhs = rhs.split('#')[0].strip()

        if 'SLIP-132' in ln:
            rhs, _, f, _ = slip132undo(rhs)
        else:
            f = None

        if rhs[1:4] == 'pub':
            expect = root.subkey_for_path(lhs)
            assert expect.hwif() == rhs
            continue

        if not f:
            if rhs[0] in '1mn':
                f = AF_CLASSIC
            elif rhs[0:4] in ['tb1q', "bc1q"]:
                f = AF_P2WPKH
            elif rhs[0:4] in ['tb1p', "bc1p"]:
                f = AF_P2TR
            elif rhs[0] in '23':
                f = AF_P2WPKH_P2SH
            else:
                raise ValueError(rhs)

        addr_vs_path(rhs, path=lhs, addr_fmt=f, chain=netcode)


@pytest.mark.qrcode
@pytest.mark.parametrize('chain', ["BTC", "XTN"])
@pytest.mark.parametrize('acct_num', [ None, 0, 99, 8989])
def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home,
                     cap_story, need_keypress, enter_number, cap_screen_qr,
                     settings_set, nfc_read_text, is_q1, press_select, press_cancel,
                     press_nfc, expect_acctnum_captured, nfc_disabled):
    # XPUB's via QR
    settings_set("chain", chain)
    chain_num = 0 if chain == "BTC" else 1
    goto_home()
    pick_menu_item('Advanced/Tools')
    pick_menu_item('Export Wallet')
    pick_menu_item('Export XPUB')

    top_items = cap_menu()
    for m in top_items:
        is_xfp = False
        if '-84' in m:
            expect = f"m/84h/{chain_num}h/{{acct}}h"
        elif '86' in m:
            expect = f"m/86h/{chain_num}h/{{acct}}h"
        elif '-44' in m:
            expect = f"m/44h/{chain_num}h/{{acct}}h"
        elif '49' in m:
            expect = f"m/49h/{chain_num}h/{{acct}}h"
        elif 'Master' in m:
            expect = "m"
        elif 'XFP' in m:
            is_xfp = True

        pick_menu_item(m)
        time.sleep(0.3)
        if is_xfp:
            got = cap_screen_qr().decode('ascii')
            time.sleep(.1)
            if not nfc_disabled():
                press_nfc()
                time.sleep(.2)
                nfc_got = nfc_read_text()
                time.sleep(.2)
                assert nfc_got == got == xfp2str(simulator_fixed_xfp).upper()
                press_cancel() # cancel animation
            press_cancel() # cancel QR
            continue

        time.sleep(0.3)
        title, story = cap_story()
        assert expect.format(acct=0) in story

        if expect != "m":
            assert "Press (1) to select account" in story
            if acct_num is not None:
                need_keypress('1')
                enter_number(acct_num)

                time.sleep(0.1)
                expect = expect.format(acct=acct_num)
                title, story = cap_story()
                assert expect in story
                assert "Press (1) to select account" in story

            expect = expect.format(acct=0)

        press_select()
        got_pub = cap_screen_qr().decode('ascii')

        if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
            assert 'NFC' in story
            press_nfc()
            time.sleep(0.2)
            got_nfc_pub = nfc_read_text()
            time.sleep(0.1)
            press_cancel() # cancel animation
            assert got_nfc_pub == got_pub

        press_cancel()  # cancel QR

        time.sleep(.1)
        _, story = cap_story()
        assert got_pub[0] in 'xt'
        if "Press (2)" in story:
            if chain == "BTC":
                assert f"{'z' if expect[:5] == 'm/84h' else 'y'}pub (SLIP-132)" in story
            else:
                assert f"{'v' if expect[:5] == 'm/84h' else 'u'}pub (SLIP-132)" in story
            need_keypress("2")
            time.sleep(.1)
            _, story = cap_story()
            assert ("%spub (BIP-32)" % ("x" if chain == "BTC" else "t")) in story
            assert "Press (2)" in story

            press_select()
            got_slip_pub = cap_screen_qr().decode('ascii')
            got_unslip, *_ = slip132undo(got_slip_pub)
            assert got_unslip == got_pub

            if f'Press {KEY_NFC if is_q1 else "(3)"}' in story:
                assert 'NFC' in story
                press_nfc()
                time.sleep(0.2)
                got_nfc_slip_pub = nfc_read_text()
                time.sleep(0.1)
                press_cancel() # cancel animation
                assert got_slip_pub == got_nfc_slip_pub

            press_cancel()  # cancel QR

        expect_acctnum_captured(acct_num)

        got = BIP32Node.from_wallet_key(got_pub)

        wallet = BIP32Node.from_wallet_key(simulator_fixed_tprv)
        if expect != 'm':
            wallet = wallet.subkey_for_path(expect[2:].replace('h', "'"))
        assert got.sec() == wallet.sec()
        press_cancel()

@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC, AF_P2TR])
@pytest.mark.parametrize("acct_num", [None, 0,  1, (2 ** 31) - 1])
@pytest.mark.parametrize("int_ext", [True, False])
def test_generic_descriptor_export(chain, addr_fmt, acct_num, goto_home,
            settings_set, need_keypress, expect_acctnum_captured, OK,
            pick_menu_item, way, cap_story, cap_menu, int_ext, settings_get,
            virtdisk_path, load_export, press_select, skip_if_useless_way, is_q1):

    skip_if_useless_way(way)

    settings_set('chain', chain)
    chain_num = 1 if chain in ["XTN", "XRT"] else 0
    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Descriptor")
    time.sleep(.1)
    _, story = cap_story()
    assert "This saves a ranged xpub descriptor" in story
    assert "Press (1) to enter a non-zero account number" in story
    assert "sensitive--in terms of privacy" in story
    assert "not compromise your funds directly" in story

    if isinstance(acct_num, int):
        need_keypress("1")        # chosse account number
        for ch in str(acct_num):
            need_keypress(ch)     # input num
        press_select()        # confirm selection
    else:
        press_select()  # confirm story

    time.sleep(.1)
    _, story = cap_story()
    assert f"To export receiving and change descriptors in one descriptor (<0;1> notation) press {OK}" in story
    assert "press (1) to export receiving and change descriptors separately" in story
    if int_ext:
        press_select()
    else:
        need_keypress("1")

    menu = cap_menu()
    if addr_fmt == AF_P2WPKH:
        menu_item = "Segwit P2WPKH"
        desc_prefix = "wpkh("
        bip44_purpose = 84
    elif addr_fmt == AF_P2WPKH_P2SH:
        menu_item = "P2SH-Segwit"
        desc_prefix = "sh(wpkh("
        bip44_purpose = 49
    elif addr_fmt == AF_P2TR:
        menu_item = "Taproot P2TR"
        desc_prefix = "tr("
        bip44_purpose = 86
    else:
        # addr_fmt == AF_CLASSIC:
        menu_item = "Classic P2PKH"
        desc_prefix = "pkh("
        bip44_purpose = 44

    assert menu_item in menu
    pick_menu_item(menu_item)

    expect_acctnum_captured(acct_num)

    time.sleep(.1)
    title, story = cap_story()
    idx = 0 if is_q1 else 1
    story_desc = story.split("\n\n")[idx]

    contents = load_export(way, label="Descriptor", is_json=False, addr_fmt=addr_fmt, sig_check=addr_fmt != AF_P2TR)
    descriptor = contents.strip()

    assert descriptor == story_desc.strip()

    if int_ext is False:
        descriptor = descriptor.split("\n")[0]  # external
    assert descriptor.startswith(desc_prefix)
    desc_obj = Descriptor.parse(descriptor)
    assert desc_obj.serialize(int_ext=int_ext) == descriptor
    assert desc_obj.addr_fmt == addr_fmt
    assert len(desc_obj.keys) == 1
    xfp, derive, xpub = desc_obj.keys[0]
    assert xfp == settings_get("xfp")
    assert derive == f"m/{bip44_purpose}h/{chain_num}h/{acct_num if acct_num is not None else 0}h"
    seed = Mnemonic.to_seed(simulator_fixed_words)
    node = BIP32Node.from_master_secret(
        seed, netcode="BTC" if chain == "BTC" else "XTN"
    ).subkey_for_path(derive)
    xpub_target = node.hwif()
    assert xpub_target in xpub


@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("way", ["nfc", "qr"])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2WPKH_P2SH])
@pytest.mark.parametrize("acct_num", [None, 55])
def test_zeus_descriptor_export(addr_fmt, acct_num, goto_home, need_keypress, pick_menu_item,
                                way, cap_story, cap_menu, nfc_read_text, settings_get, chain,
                                virtdisk_path, load_export, press_select, skip_if_useless_way,
                                settings_set, is_q1, press_cancel, cap_screen_qr, press_nfc,
                                expect_acctnum_captured):

    skip_if_useless_way(way)

    settings_set('chain', chain)
    chain_num = 1 if chain == "XTN" else 0

    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Zeus")
    time.sleep(.1)
    title, story = cap_story()

    assert "This saves a ranged xpub descriptor" in story
    assert "Press (1) to enter a non-zero account number" in story
    assert "sensitive--in terms of privacy" in story
    assert "not compromise your funds directly" in story

    if isinstance(acct_num, int):
        need_keypress("1")        # chosse account number
        for ch in str(acct_num):
            need_keypress(ch)     # input num
        press_select()        # confirm selection
    else:
        press_select()  # confirm story

    time.sleep(.1)
    menu = cap_menu()
    assert len(menu) == 2
    if addr_fmt == AF_P2WPKH:
        menu_item = "Segwit P2WPKH"
        desc_prefix = "wpkh("
        bip44_purpose = 84
    else:
        assert addr_fmt == AF_P2WPKH_P2SH
        menu_item = "P2SH-Segwit"
        desc_prefix = "sh(wpkh("
        bip44_purpose = 49

    assert menu_item in menu
    pick_menu_item(menu_item)

    time.sleep(.1)
    title, story = cap_story()
    idx = 0 if is_q1 else 1
    story_desc = story.split("\n\n")[idx]

    expect_acctnum_captured(acct_num)

    if way == "qr":
        assert ("%s to show QR" % (KEY_QR if is_q1 else "(4)")) in story
        need_keypress(KEY_QR if is_q1 else "4")
        time.sleep(.2)
        contents = cap_screen_qr().decode('ascii')
    else:
        assert ("ress %s to share via NFC" % (KEY_NFC if is_q1 else "(3)")) in story
        press_nfc()
        time.sleep(.2)
        contents = nfc_read_text()
        time.sleep(.5)
        press_cancel()  # exit NFC animation

    descriptor = contents.strip()

    assert descriptor == story_desc.strip()

    assert descriptor.startswith(desc_prefix)
    desc_obj = Descriptor.parse(descriptor)
    assert desc_obj.serialize(int_ext=True) == descriptor
    assert desc_obj.addr_fmt == addr_fmt
    assert len(desc_obj.keys) == 1
    xfp, derive, xpub = desc_obj.keys[0]
    assert xfp == settings_get("xfp")
    assert derive == f"m/{bip44_purpose}h/{chain_num}h/{acct_num if acct_num is not None else 0}h"
    seed = Mnemonic.to_seed(simulator_fixed_words)
    node = BIP32Node.from_master_secret(
        seed, netcode="BTC" if chain == "BTC" else "XTN"
    ).subkey_for_path(derive)
    xpub_target = node.hwif()
    assert xpub_target in xpub


@pytest.mark.parametrize("chain", ["BTC", "XTN"])
def test_bullbitcoin_descriptor_export(goto_home, need_keypress, pick_menu_item,
                                       cap_story, cap_menu, nfc_read_text, settings_get, chain,
                                       press_select, skip_if_useless_way,
                                       settings_set, press_cancel, cap_screen_qr,
                                       expect_acctnum_captured):

    settings_set('chain', chain)
    chain_num = 1 if chain == "XTN" else 0

    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Bull Bitcoin")

    time.sleep(.1)
    expect_acctnum_captured(0)

    contents = cap_screen_qr().decode('ascii')
    descriptor = contents.strip()

    press_cancel()
    time.sleep(.1)
    assert "Bull Bitcoin" in cap_menu()  # back to menu

    assert descriptor.startswith("wpkh(")
    desc_obj = Descriptor.parse(descriptor)
    assert desc_obj.serialize(int_ext=True) == descriptor
    assert desc_obj.addr_fmt == AF_P2WPKH
    assert len(desc_obj.keys) == 1
    xfp, derive, xpub = desc_obj.keys[0]
    assert xfp == settings_get("xfp")
    assert derive == f"m/84h/{chain_num}h/0h"
    seed = Mnemonic.to_seed(simulator_fixed_words)
    node = BIP32Node.from_master_secret(
        seed, netcode="BTC" if chain == "BTC" else "XTN"
    ).subkey_for_path(derive)
    xpub_target = node.hwif()
    assert xpub_target in xpub


@pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"])
@pytest.mark.parametrize("account", ["Postmix", "Premix"])
def test_samourai_vs_generic(chain, account, settings_set, pick_menu_item, goto_home,
                             need_keypress, cap_story, microsd_path, nfc_read_text,
                             load_export, press_select, press_cancel):
    if account == "Postmix":
        acct_num = 2147483646
        in_story = "Samourai POST-MIX"
    else:
        acct_num = 2147483645
        in_story = "Samourai PRE-MIX"

    settings_set('chain', chain)
    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Descriptor")

    need_keypress("1")
    for ch in str(acct_num):
        need_keypress(ch)
    press_select()
    press_select()  # int_ext <0;1>
    pick_menu_item("Segwit P2WPKH")  #  both postmix and premix are p2wpkh only
    file_desc_generic = load_export("sd", label="Descriptor", is_json=False, addr_fmt=AF_P2WPKH)
    press_select()  # written
    press_cancel()  # leave export options
    press_cancel()  # back to export submenu
    press_cancel()  # back to advanced
    pick_menu_item("Export Wallet")
    pick_menu_item(f"Samourai {account}")
    time.sleep(.1)
    _, story = cap_story()
    assert "This saves a ranged xpub descriptor" in story
    assert in_story in story
    assert "Press 1 to enter a non-zero account number" not in story  # NOT
    assert "sensitive--in terms of privacy" in story
    assert "not compromise your funds directly" in story
    press_select()
    file_desc = load_export("sd", label="Descriptor", is_json=False, addr_fmt=AF_P2WPKH)
    assert file_desc.strip() == file_desc_generic.strip()


@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("way", ["sd", "vdisk", "nfc", "qr"])
@pytest.mark.parametrize("addr_fmt", [AF_P2WPKH, AF_P2TR, AF_P2WPKH_P2SH, AF_CLASSIC,
                                      AF_P2WSH, AF_P2WSH_P2SH, 1000])  # using 1000 as P2TR multisig
@pytest.mark.parametrize("acct_num", [None, (2 ** 31) - 1])
def test_key_expression_export(chain, addr_fmt, acct_num, goto_home, settings_set, need_keypress,
                               pick_menu_item, way, cap_story, cap_menu, virtdisk_path, dev,
                               load_export, press_select, skip_if_useless_way, is_q1):

    skip_if_useless_way(way, allow_mk4_qr=True)

    settings_set('chain', chain)
    chain_num = 1 if chain in ["XTN", "XRT"] else 0
    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Key Expression")
    time.sleep(.1)
    _, story = cap_story()
    assert "This saves a extended key expression" in story
    assert "Press (1) to enter a non-zero account number" in story
    assert "sensitive--in terms of privacy" in story
    assert "not compromise your funds directly" in story

    if isinstance(acct_num, int):
        need_keypress("1")        # chosse account number
        for ch in str(acct_num):
            need_keypress(ch)     # input num
        press_select()        # confirm selection
    else:
        press_select()  # confirm story
        acct_num = 0

    sig_check = True
    menu = cap_menu()
    if addr_fmt == AF_P2WPKH:
        menu_item = "Segwit P2WPKH"
        derive = f"m/84h/{chain_num}h/{acct_num}h"
    elif addr_fmt == AF_P2TR:
        menu_item = "Taproot P2TR"
        derive = f"m/86h/{chain_num}h/{acct_num}h"
        sig_check = False
    elif addr_fmt == AF_P2WPKH_P2SH:
        menu_item = "P2SH-Segwit"
        derive = f"m/49h/{chain_num}h/{acct_num}h"
    elif addr_fmt == AF_CLASSIC:
        menu_item = "Classic P2PKH"
        derive = f"m/44h/{chain_num}h/{acct_num}h"
    elif addr_fmt == AF_P2WSH:
        menu_item = "Multi P2WSH"
        derive = f"m/48h/{chain_num}h/{acct_num}h/2h"
        addr_fmt = AF_CLASSIC
    elif addr_fmt == AF_P2WSH_P2SH:
        menu_item = "Multi P2SH-P2WSH"
        derive = f"m/48h/{chain_num}h/{acct_num}h/1h"
        addr_fmt = AF_CLASSIC
    else:
        assert addr_fmt == 1000
        menu_item = "Multi P2TR"
        derive = f"m/48h/{chain_num}h/{acct_num}h/3h"
        addr_fmt = AF_CLASSIC

    assert menu_item in menu
    pick_menu_item(menu_item)

    time.sleep(.1)
    title, story = cap_story()
    idx = 0 if is_q1 else 1
    story_key_exp = story.split("\n\n")[idx]

    contents = load_export(way, label="Key Expression", is_json=False, addr_fmt=addr_fmt,
                           sig_check=sig_check)
    key_exp = contents.strip()

    xfp = dev.master_fingerprint
    xfp = xfp2str(xfp).lower()

    seed = Mnemonic.to_seed(simulator_fixed_words)
    node = BIP32Node.from_master_secret(
        seed, netcode="BTC" if chain == "BTC" else "XTN"
    ).subkey_for_path(derive)

    target = f"[{xfp}/{derive.replace('m/', '')}]{node.hwif()}"
    assert key_exp == target == story_key_exp


@pytest.mark.parametrize('path', [
    # NOTE: (2**31)-1 = 0x7fff_ffff = 2147483647
    "m/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647/2147483647",
    "m/1/2/3/4/5",
    "m/1h/2h/3h/4h/5h",
    "m/45h",
])
def test_custom_key_expression_export(path, goto_home, pick_menu_item, cap_menu, need_keypress,
                                      press_select, load_export, use_testnet, dev, cap_story, is_q1):
    use_testnet()
    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Export Wallet")
    pick_menu_item("Key Expression")
    press_select() # story
    pick_menu_item("Custom Path")

    # blind entry, using only first 2 menu items
    deeper = path.split("/")[1:]
    for depth, part in enumerate(deeper):
        time.sleep(.01)
        m = cap_menu()
        for mi in m:
            assert "{idx}" not in mi  # ranged values not allowed here
        if depth == 0:
            assert m[0] == 'm/⋯'
            pick_menu_item(m[0])
        else:
            assert m[0].endswith("h/⋯")
            assert m[1].endswith("/⋯")
            assert m[0] != m[1]

            pick_menu_item(m[0 if last_part[-1] == "h" else 1])

        # enter path component
        for d in part:
            if d == "h": break
            need_keypress(d)
        press_select()

        last_part = part

    time.sleep(.01)
    m = cap_menu()
    pick_menu_item(m[2 if part[-1] == "h" else 3])

    time.sleep(.25)
    title, story = cap_story()
    idx = 0 if is_q1 else 1
    story_key_exp = story.split("\n\n")[idx]

    contents = load_export("sd", label="Key Expression", is_json=False)
    key_exp = contents.strip()

    xfp = dev.master_fingerprint
    xfp = xfp2str(xfp).lower()

    seed = Mnemonic.to_seed(simulator_fixed_words)
    node = BIP32Node.from_master_secret(seed, netcode="XTN").subkey_for_path(path)

    target = f"[{xfp}/{path.replace('m/', '')}]{node.hwif()}"
    assert key_exp == target == story_key_exp

# EOF
