# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# test drv_entro.py features
#
import pytest, time, re
from binascii import a2b_hex, b2a_hex
from bip32 import BIP32Node, PrivateKey
from mnemonic import Mnemonic
from charcodes import KEY_QR

HISTORY = set()

# XPRV from spec: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
EXAMPLE_XPRV = '011b67969d1ec69bdfeeae43213da8460ba34b92d0788c8f7bfcfa44906e8a589c3f15e5d852dc2e9ba5e9fe189a8dd2e1547badef5b563bbe6579fc6807d80ed900000000000000'


@pytest.fixture
def derive_bip85_secret(goto_home, press_select, pick_menu_item, cap_story, enter_text,
                        set_encoded_secret, set_seed_words, settings_set, is_q1,
                        seed_story_to_words):
    def doit(mode, index, expect=None, entropy=None, sim_sec=None, chain="BTC"):
        if sim_sec:
            if len(sim_sec.split(" ")) in (12,18,24):
                set_seed_words(sim_sec)
            else:
                set_encoded_secret(a2b_hex(sim_sec))

        if chain:
            settings_set('chain', chain)

        goto_home()
        time.sleep(.1)
        pick_menu_item('Advanced/Tools')
        time.sleep(.1)
        pick_menu_item('Derive Seed B85' if not is_q1 else 'Derive Seeds (BIP-85)')

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

        assert 'seed value' in story
        assert 'other wallet systems' in story

        press_select()
        time.sleep(0.1)
        title, story = cap_story()
        if "You have a temporary seed active - deriving from temporary" in story:
            press_select()

        time.sleep(0.1)
        pick_menu_item(mode)

        enter_text(str(index) if index is not None else '')

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

        assert f'Path Used (index={index}):' in story
        assert "m/83696968h/" in story
        assert f"/{index}h" in story

        if entropy is not None:
            assert f"Raw Entropy:\n{entropy}" in story

        can_import = False

        if ' words' in mode:
            num_words = int(mode.split()[0])
            assert f'Seed words ({num_words}):' in story
            assert f"m/83696968h/39h/0h/{num_words}h/{index}h" in story
            assert '1:' in story
            assert f'{num_words}:' in story
            got = seed_story_to_words(story)
            if expect:
                assert ' '.join(got) == expect
            can_import = 'words'

            seed = Mnemonic.to_seed(' '.join(got))
            node = BIP32Node.from_master_secret(seed)
            assert node.fingerprint().hex().upper() in title
            assert "(6) to type words over USB" in story

        elif 'XPRV' in mode:
            assert 'Derived XPRV:' in story
            assert f"m/83696968h/32h/{index}h" in story
            if expect:
                assert expect in story
            can_import = 'xprv'

            node = BIP32Node.from_hwif(story.split("\n\n")[0].split("\n")[-1])
            assert node.fingerprint().hex().upper() in title
            assert "(6) to type xprv over USB" in story

        elif 'WIF' in mode:
            assert 'WIF (privkey)' in story
            assert f"m/83696968h/2h/{index}h" in story
            if expect:
                assert expect in story
            assert "(6) to type wif over USB" in story

        elif 'bytes hex' in mode:
            width = int(mode.split('-')[0])
            assert width in {32, 64}
            assert f'Hex ({width} bytes):' in story
            assert f"m/83696968h/128169h/{width}h/{index}h" in story
            if expect:
                assert expect in story
            assert "(6) to type hex over USB" in story

        elif 'Passwords' == mode:
            assert "Password:" in story
            assert f"m/83696968h/707764h/21h/{index}h" in story
            if expect:
                assert expect in story
            assert "(6) to type pw over USB" in story

        else:
            raise ValueError(mode)

        return can_import, story

    return doit


@pytest.fixture
def activate_bip85_ephemeral(need_keypress, cap_story, sim_exec, reset_seed_words,
                             confirm_tmp_seed):
    def doit(do_import, reset=True, expect=None, entropy=None, save_to_vault=False):
        _, story = cap_story()
        assert '(0) to switch to derived secret' in story

        try:
            time.sleep(0.1)
            need_keypress('0')

            confirm_tmp_seed(seedvault=save_to_vault)

            encoded = sim_exec('from pincodes import pa; RV.write(repr(pa.fetch()))')
            print(encoded)
            assert 'Error' not in encoded
            encoded = eval(encoded)
            assert len(encoded) == 72

            marker = encoded[0]
            if do_import == 'words':
                assert marker & 0x80 == 0x80
                width = ((marker & 0x3) + 2) * 8
                assert width in {16, 24, 32}
                if entropy:
                    assert encoded[1:1 + width] == a2b_hex(entropy)
            elif do_import == 'xprv':
                assert marker == 0x01
                if expect:
                    node = BIP32Node.from_hwif(expect)
                    ch, pk = encoded[1:33], encoded[33:65]
                    assert node.chain_code() == ch
                    assert bytes(node.node.private_key) == pk

        finally:
            # required cleanup
            if reset:
                reset_seed_words()

    return doit


@pytest.mark.parametrize('mode,index,entropy,expect', [ 
    ('12 words', 0,
        '6250b68daf746d12a24d58b4787a714b', 
        'girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose'),
    ('18 words', 0,
        '938033ed8b12698449d4bbca3c853c66b293ea1b1ce9d9dc',
         'near account window bike charge season chef number sketch tomorrow excuse sniff circle vital hockey outdoor supply token'),
    ('24 words', 0,
        'ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f',
        'puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano'),

    ('WIF (privkey)', 0,
        '7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1',
        'Kzyv4uF39d4Jrw2W7UryTHwZr1zQVNk4dAFyqE6BuMrMh1Za7uhp'),
    ('XPRV (BIP-32)', 0,
        None,
        'xprv9s21ZrQH143K2srSbCSg4m4kLvPMzcWydgmKEnMmoZUurYuBuYG46c6P71UGXMzmriLzCCBvKQWBUv3vPB3m1SATMhp3uEjXHJ42jFg7myX'),
    ('32-bytes hex', 0,
        None,
        'ea3ceb0b02ee8e587779c63f4b7b3a21e950a213f1ec53cab608d13e8796e6dc'),
    ('64-bytes hex', 0,
        None,
        '492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e71aa2d645442f878555d078fd1f1f67e368976f04137b1f7a0d19232136ca50c44614af72b5582a5c'),

    ('Passwords', 0,
     None,
     "dKLoepugzdVJvdL56ogNV"),
])
def test_bip_vectors(mode, index, entropy, expect, cap_story, need_keypress,
                     load_export_and_verify_signature, derive_bip85_secret,
                     activate_bip85_ephemeral, press_select, press_cancel):

    do_import, story = derive_bip85_secret(mode, index, expect, entropy, sim_sec=EXAMPLE_XPRV)

    # write to SD
    msg = story.split('Press', 1)[0]
    assert 'Press (1) to save' in story
    need_keypress('1')

    time.sleep(0.1)
    title, story = cap_story()
    contents,_, _ = load_export_and_verify_signature(story, "sd", fpattern="drv", label=None)
    assert contents.strip() == msg.strip()
    press_select()
    time.sleep(0.1)
    title, story = cap_story()

    if do_import:
        activate_bip85_ephemeral(do_import, expect=expect, entropy=entropy)
    else:
        assert 'show QR code' in story

    press_cancel()


def test_allow_bip32_max_int(pick_menu_item, goto_home, enter_number, is_q1,
                             press_select, press_cancel, cap_story,
                             settings_set):
    max_int = 2147483647
    to_input = 9999999999
    mi = 'Derive Seed B85' if not is_q1 else 'Derive Seeds (BIP-85)'
    goto_home()
    # by default only indexes up to 9999 are allowed
    pick_menu_item("Advanced/Tools")
    pick_menu_item(mi)
    press_select()
    pick_menu_item("12 words")
    enter_number(to_input)
    time.sleep(.1)
    _, story = cap_story()
    assert "index=9999" in story  # does not allow to go over this value
    press_cancel()

    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item("Danger Zone")
    pick_menu_item("B85 Idx Values")
    time.sleep(.1)
    _, story = cap_story()
    assert "Allow unlimited indexes for BIP-85 derivations?" in story
    assert "DANGER" in story
    press_select()
    pick_menu_item("Unlimited")

    goto_home()
    pick_menu_item("Advanced/Tools")
    pick_menu_item(mi)
    press_select()
    pick_menu_item("12 words")
    enter_number(to_input)
    time.sleep(.1)
    _, story = cap_story()
    assert f"index={max_int}" in story
    press_cancel()
    settings_set("b85max", 0)


@pytest.mark.qrcode
@pytest.mark.parametrize('mode,pattern', [ 
    ('WIF (privkey)', r'[1-9A-HJ-NP-Za-km-z]{51,52}' ),
    ('XPRV (BIP-32)', r'[tx]prv[1-9A-HJ-NP-Za-km-z]{107}'),
    ('32-bytes hex', r'[a-f0-9]{64}'),
    ('64-bytes hex', r'[a-f0-9]{128}'),
    ('12 words', r'[a-f0-9]{32}'),
    ('18 words', r'[a-f0-9]{48}'),
    ('24 words', r'[a-f0-9]{64}'),
    ('Passwords', r'[a-zA-Z0-9+/]{21}'),
])
@pytest.mark.parametrize('index', [0, 1, 10, 100, 1000, 9999, 2147483647])
def test_path_index(mode, pattern, index, need_keypress, cap_screen_qr, seed_story_to_words,
                    derive_bip85_secret, reset_seed_words, is_q1, press_cancel, settings_set):
    reset_seed_words()
    settings_set("b85max", 1)

    # Uses any key on Simulator; just checking for operation + entropy level
    _, story = derive_bip85_secret(mode, index)

    assert f'Path Used (index={index}):' in story
    assert "m/83696968h/" in story
    assert f"/{index}h" in story

    got = re.findall(pattern, story)[0]

    assert len(set(got)) >= 12

    global HISTORY
    assert got not in HISTORY
    HISTORY.add(got)

    if mode == "Passwords":
        from base64 import b64encode
        raw = re.findall(r'[a-f0-9]{64}', story)[0]
        exp = b64encode(a2b_hex(raw)).decode('ascii')[0:21]
        assert exp == got
    elif 'words' in mode:
        exp = Mnemonic('english').to_mnemonic(a2b_hex(got)).split()
        assert seed_story_to_words(story) == exp
    elif 'XPRV' in mode:
        node = BIP32Node.from_hwif(got)
        assert str(b2a_hex(node.chain_code()), 'ascii') in story
        assert bytes(node.node.private_key).hex() in story
    elif 'WIF' in mode:
        key = PrivateKey.from_wif(got)
        assert bytes(key).hex() in story

    if index == 0:
        assert 'show QR code' in story
        need_keypress(KEY_QR if is_q1 else '4')

        qr = cap_screen_qr().decode('ascii')

        if mode == "Passwords":
            assert qr == exp == got
        elif 'words' in mode:
            gw = qr.lower().split()
            assert gw == [i[0:4] for i in exp]

        elif 'hex' in mode:
            assert qr.lower() == got

        elif 'XPRV' in mode:
            assert qr == got

        elif 'WIF' in mode:
            assert qr == got

    press_cancel()
    settings_set("b85max", 0)


def test_type_passwords(dev, cap_menu, pick_menu_item, goto_home, OK,
                        cap_story, press_select, cap_screen, enter_text):
    goto_home()
    pick_menu_item('Settings')
    pick_menu_item('Keyboard EMU')
    _, story = cap_story()
    story1 = "This mode adds a top-level menu item for typing deterministically-generated passwords (BIP-85), directly into an attached USB computer (as an emulated keyboard)."
    assert story1 == story
    press_select()
    pick_menu_item('Enable')
    time.sleep(0.3)
    goto_home()
    menu = cap_menu()
    assert "Type Passwords" in menu
    pick_menu_item("Type Passwords")
    time.sleep(0.1)
    # here we accessed index loop and can derive
    for index in [0, 10, 100, 1000, 9999]:
        time.sleep(0.5)
        enter_text(str(index))
        time.sleep(1)
        _, story = cap_story()
        assert f"Place mouse at required password prompt, then press {OK} to send keystrokes." in story
        split_story = story.split("\n\n")
        _, pwd = split_story[1].split("\n")
        _, path = split_story[2].split("\n")
        assert path == f"m/83696968h/707764h/21h/{index}h"
        assert len(pwd) == 21
        assert "=" not in pwd
        press_select()  # does nothing on simulator
        time.sleep(0.2)

    # exit Enter Password menu
    goto_home()
    pick_menu_item('Settings')
    pick_menu_item('Keyboard EMU')
    pick_menu_item('Default Off')
    menu = cap_menu()
    assert "Type Passwords" not in menu

def test_export_nfc_when_disabled(pick_menu_item, goto_home, cap_story, press_select,
                                  is_q1, derive_bip85_secret, press_nfc, cap_menu):
    goto_home()
    pick_menu_item("Settings")
    pick_menu_item("Hardware On/Off")
    pick_menu_item("NFC Sharing")
    time.sleep(.1)
    _, story = cap_story()
    if "Near Field Communications" in story:
        press_select()

    pick_menu_item("Default Off")
    time.sleep(.1)
    goto_home()
    _, story = derive_bip85_secret('12 words', 999)
    assert "NFC" not in story
    press_nfc()
    time.sleep(.1)
    goto_home()
    m = cap_menu()
    assert "Ready To Sign" in m


# EOF
