# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# UX and interactions related to Settings > Pin Options
#
# - altho this can pass on the simulator, it's much more interesting to test with "bare metal"
# - need this to recover:
#       from main import pa; pa.change(is_duress=1, new_pin=b'', old_pin=b'77-77')
# - same tests are needed in three modes: normal login, secondary PIN used for login, and
#   duress PIN used for login.
# - my convention for known PINS:
#       12-12       main wallet
#       23-23       secondary wallet (mk1/2)
#       33-33       main duress wallet
#       66-66       brickme
#
import pytest, time, os
from helpers import xfp2str

CLR_PIN = '999999-999999'

@pytest.fixture(scope='session')
def under_duress(request):
    # add flag: --duress to commandline to indicate this mode
    return request.config.getoption('duress') 

@pytest.fixture
def get_duress_secret(sim_exec):
    def doit(pin):
        # read the duress secret
        rv = sim_exec(f'from pincodes import pa; RV.write(repr(pa.fetch(duress_pin=b"{pin}")))')
        if rv.startswith('Traceback'):
            raise RuntimeError(rv)
        assert rv[0:2] == "b'"
        return eval(rv)
    return doit

@pytest.fixture
def verify_pin_set(sim_exec):
    def doit(pin, secondary=False, duress=False, brickme=False):
        # check the SE holds the indicated PIN code
        kws = ''
        if secondary:
            kws += ", is_secondary=1"
        if duress:      # not used
            kws += ", is_duress=1"
        if brickme:
            kws += ", is_brickme=1"
        rv = sim_exec(f'from pincodes import pa; RV.write(repr(pa.change(new_pin=b"{pin}", old_pin=b"{pin}" {kws})))')
        if rv != 'None':
            raise RuntimeError(rv)

    return doit

@pytest.fixture
def get_secondary_login(sim_exec):
    def doit():
        return sim_exec('from pincodes import pa; RV.write(repr(pa.is_secondary))') == 'True'
    return doit

@pytest.fixture
def goto_pin_options(pick_menu_item, goto_home):
    def doit():
        goto_home()
        pick_menu_item('Settings')
        pick_menu_item('Login Settings')

    return doit

@pytest.fixture
def my_enter_pin(cap_screen, need_keypress):
    def doit(pin):
        time.sleep(.01)      # required?
        scr = cap_screen().split('\n')
        title = scr[1]
        assert scr[2] == 'Enter PIN Prefix'
        for ch in pin:
            if ch != '-':
                time.sleep(.05)      # required?
                need_keypress(ch)
                time.sleep(.05)      # required?
                continue

            if ch == '-':
                need_keypress('y')
                time.sleep(.1)      # required

                scr = cap_screen().split('\n')
                assert ('Recognize these?' in scr) or ('Write these down:' in scr)
                words = scr[2:4]
                need_keypress('y')

                time.sleep(.1)      # required
                scr = cap_screen().split('\n')
                assert scr[-1] == 'Enter rest of PIN'

        need_keypress('y')
        time.sleep(0.1)

        return title, words


    return doit

@pytest.fixture
def change_pin(cap_screen, cap_story, cap_menu, need_keypress, my_enter_pin):
    def doit(old_pin, new_pin, hdr_text, expect_fail=None):
        # use standard menus and UX to change a PIN 
        title, story = cap_story()
        assert title == hdr_text
        assert ('We strongly recommend' in story) or (CLR_PIN in story)
        need_keypress('y')
        time.sleep(0.01)      # required

        assert max(len(i) for i in new_pin.split('-')) <= 6
        assert 2 <= min(len(i) for i in new_pin.split('-'))

        # give old pin, if there was one
        if old_pin != None:
            title, words = my_enter_pin(old_pin)
            assert title == 'Old '+hdr_text

        title, words2 = my_enter_pin(new_pin)
        if old_pin == None and title == 'Old '+hdr_text:
            raise ValueError("PIN was set, but we though it wouldnt be")
        assert title == 'New '+hdr_text

        # confirm, if not clearing the PIN
        if new_pin != CLR_PIN:
            title, words3 = my_enter_pin(new_pin)
            assert title == 'New '+hdr_text
            assert words2 == words3

        if expect_fail:
            title, story = cap_story()
            assert title == 'Try Again'
            assert expect_fail in story
            need_keypress('x')
            return

        # saving/verifying can take tens of seconds.
        time.sleep(5) 
        for retries in range(10):
            try:
                if 'Test Login Now' in cap_menu():
                    break
            except:
                # USB not ready when busy in bootloader code
                pass
            time.sleep(1)
        else:
            raise pytest.fail("Menu didn't come back")

        return words2

    return doit

@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
def test_main_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, under_duress):
    goto_pin_options()

    try:
        pick_menu_item('Change Main PIN')
    except KeyError:
        # secondary login, for example
        assert get_secondary_login()
        raise pytest.skip('cant change main from secondary')

    DEF_PIN = '12-12' if not under_duress else '33-33'

    change_pin(DEF_PIN, new_pin, 'Main PIN')
    verify_pin_set(new_pin)

    pick_menu_item('Change Main PIN')
    change_pin(new_pin, DEF_PIN, 'Main PIN')
    verify_pin_set(DEF_PIN)

@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
def test_duress_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, get_duress_secret, under_duress, only_mk3):
    goto_pin_options()


    pick_menu_item('Duress PIN')
    try:
        change_pin(None, new_pin, 'Duress PIN')
    except ValueError:
        if under_duress:
            raise pytest.xfail("cant change duress while under duress")
        else:
            raise

    # duress secret should be complex
    d_secret = get_duress_secret(new_pin)
    assert len(set(d_secret)) > 20
    assert d_secret[0] == 0x01, "not xprv?"

    # changing PIN shouldn't change secret
    pick_menu_item('Duress PIN')
    change_pin(new_pin, '123-123', 'Duress PIN')
    rb = get_duress_secret('123-123')
    assert rb == d_secret

    pick_menu_item('Duress PIN')
    change_pin('123-123', CLR_PIN, 'Duress PIN')

    # clearing PIN should clear secret
    zz = get_duress_secret('')
    assert zz == b'\0'*72

@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
def test_secondary_pin(is_mark3, goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, only_mk3):

    if get_secondary_login():
        raise pytest.skip('not intended for use under secondary login')

    goto_pin_options()

    if is_mark3:
        raise pytest.skip('mark3 doesnt support secondary wallet')

    pick_menu_item('Second Wallet')
    try:
        change_pin(None, new_pin, 'Second PIN')
    except ValueError:
        if under_duress:
            raise pytest.xfail("cant change secondary while under duress")
        else:
            raise
    verify_pin_set(new_pin, secondary=1)

    pick_menu_item('Second Wallet')
    change_pin(new_pin, CLR_PIN, 'Second PIN')
    verify_pin_set('', secondary=1)

@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
def test_secondary_from_secondary_pin(is_mark3, goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, only_mk3):
    # XXX Obsolete now?

    # when logged into secondary wallet, you can't clear PIN, and we use a 23-23 as value
    if not get_secondary_login():
        raise pytest.skip('intended for use under secondary login')
    if is_mark3:
        raise pytest.skip('mark3 doesnt support secondary wallet')

    goto_pin_options()

    ASSUME_PIN = '23-23'

    pick_menu_item('Second Wallet')
    change_pin(ASSUME_PIN, new_pin, 'Second PIN')
    verify_pin_set(new_pin)

    pick_menu_item('Second Wallet')
    change_pin(new_pin, ASSUME_PIN, 'Second PIN')
    verify_pin_set(ASSUME_PIN)

@pytest.mark.parametrize('new_pin', ['77-77', '123456-654321', '79-654321', '123456-12'])
def test_brickme_pin(goto_pin_options, pick_menu_item, cap_story, cap_screen, need_keypress, change_pin, new_pin, verify_pin_set, get_secondary_login, under_duress, only_mk3):

    goto_pin_options()

    try:
        pick_menu_item('Brick Me PIN')
    except KeyError:
        # secondary login, for example
        assert get_secondary_login()
        raise pytest.skip('cant do brickme in this mode')

    try:
        change_pin(None, new_pin, 'Brickme PIN')
    except ValueError:
        if under_duress:
            raise pytest.xfail("cant change brickme while under duress")
        else:
            raise

    verify_pin_set(new_pin, brickme=1)

    pick_menu_item('Brick Me PIN')
    change_pin(new_pin, CLR_PIN, 'Brickme PIN')
    verify_pin_set('', brickme=1)

# EOF
