# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# tests for ../shared/notes.py
#
import pytest, time, json, random, os, pdb
from helpers import prandom
from charcodes import *
from constants import AF_CLASSIC, AF_P2WPKH_P2SH, AF_P2WPKH
from bbqr import split_qrs


# All tests in this file are exclusively meant for Q
#
@pytest.fixture(autouse=True)
def THIS_FILE_requires_q1(is_q1):
    if not is_q1:
        raise pytest.skip('Q1 only')

@pytest.fixture
def goto_notes(cap_story, cap_menu, press_select, goto_home, pick_menu_item):
    # drill to the notes menu
    def doit(item=None):
        mt = 'Secure Notes & Passwords'
        goto_home()  # TODO this is probably why all menus are properly generated
        m = cap_menu()
        if mt in m:
            pick_menu_item(mt)
        else:
            pick_menu_item('Advanced/Tools')
            pick_menu_item(mt)

            title, story = cap_story()
            if title == 'Secure Notes':
                # enable feature
                press_select()

        if item:
            pick_menu_item(item)

    return doit

@pytest.fixture
def need_some_notes(is_q1, settings_get, settings_set):
    # create a note or use what's there, provide as obj
    def doit(title='Title Here', body='Body'):
        assert is_q1
        notes = settings_get('notes', [])
        if not notes:
            settings_set('notes', [dict(misc=body, title=title)])
            settings_set('secnap', True)
        return notes
    return doit

@pytest.fixture
def need_some_passwords(settings_get, settings_set):
    def doit():
        notes = settings_get('notes', [])
        if not any(1  for n in notes if n.get('password', False)):
            notes.extend([
                {'misc': 'More Notes AAAA',
                 'password': 'fds65fd5f1sd51s',
                 'site': 'https://a.com',
                 'title': 'A',
                 'user': 'AAA'},
                {'misc': 'More Notes BBB',
                 'password': 'default',
                 'site': 'www.site.b.com',
                 'title': 'B-Title',
                 'user': 'Buzzer'}
            ])
            settings_set('notes', notes)
            settings_set('secnap', True)
        return notes
    return doit

@pytest.fixture
def delete_note(press_select, goto_notes, cap_menu, pick_menu_item,
                cap_story):
    def doit(n_title):
        goto_notes()
        m = cap_menu()
        found = [i for i in m if f': {n_title}' in i]
        assert found
        pick_menu_item(found[-1])

        pick_menu_item('Delete')
        title, story = cap_story()
        assert 'SURE' in title
        assert 'Everything about this' in story

        # back to top notes menu
        press_select()

    return doit

@pytest.fixture
def build_note(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story,
               need_keypress, cap_screen_qr, readback_bbqr, nfc_read_text,
               press_select, press_cancel, is_headless, nfc_disabled):

    def doit(n_title, n_body):
        # we don't try to preserve leading/trailing spaces on note bodies
        n_body= n_body.strip()

        goto_notes('New Note')

        # create
        enter_text(n_title)
        enter_text(n_body, multiline=True)

        # view
        time.sleep(0.1)
        m = cap_menu()
        assert m[0] == f'"{n_title}"'
        assert m[1] == 'View Note'
        assert m[2] == 'Edit'
        assert m[3] == 'Delete'
        assert m[4] == 'Export'

        # test readback
        for mi in ['View Note', f'"{n_title}"']:
            time.sleep(0.1)
            pick_menu_item(mi)
            time.sleep(1)
            title, story = cap_story()
            assert title == n_title
            assert story == n_body

            if not is_headless:
                need_keypress(KEY_QR)
                qr_rb = cap_screen_qr().decode('utf-8')
                assert qr_rb == n_body

            press_cancel()

        # hidden QR button on menu feature
        m = cap_menu()
        assert m[1] == 'View Note'
        if not is_headless:
            need_keypress(KEY_QR)
            qr_rb = cap_screen_qr().decode('utf-8')
            assert qr_rb == n_body
            press_cancel()

        # hidden NFC button on menu feature
        m = cap_menu()
        assert m[1] == 'View Note'
        if not nfc_disabled:
            need_keypress(KEY_NFC)
            time.sleep(.1)
            nfc_rb = nfc_read_text()
            time.sleep(.1)
            assert nfc_rb == n_body
            press_cancel()

        # export
        pick_menu_item('Export')
        title, story = cap_story()
        assert 'Export' in title
        assert 'to save note to SD' in story
        assert 'to show QR' in story
        assert 'WARNING' in story
        assert 'will be cleartext' in story

        need_keypress(KEY_QR)
        file_type, data = readback_bbqr()
        assert file_type == 'J'
        obj = json.loads(data)
        assert obj.keys() == {'coldcard_notes'}
        obj = obj['coldcard_notes']
        assert len(obj) == 1
        obj = obj[0]
        assert obj['title'] == n_title
        assert obj['misc'] == n_body

        # back to top notes menu
        press_select()

    return doit


@pytest.fixture
def build_password(goto_notes, pick_menu_item, enter_text, cap_menu, cap_story,
                   need_keypress, cap_screen_qr, nfc_read_text,
                   cap_text_box, settings_get, settings_set, scan_a_qr,
                   press_select, press_cancel, is_headless):

    def doit(n_title, n_user=None, n_pw='secret', n_site=None, n_body=None, key_pw=None):
        goto_notes('New Password')
        enter_text(n_title)
        if n_user:
            enter_text(n_user)
        else:
            press_select()

        if key_pw and key_pw == KEY_QR:
            need_keypress(KEY_QR)
            time.sleep(1.1)
            scan_a_qr(n_pw)
            time.sleep(1.1)

        elif key_pw:
            # function keys: let it auto gen
            need_keypress(key_pw)
            time.sleep(0.1)
            if key_pw == KEY_F5:       # bip-85
                enter_text('34')
                time.sleep(0.1)
            n_pw = ''.join(cap_text_box()).strip()
            assert n_pw and len(n_pw) > 10
            need_keypress(KEY_ENTER)
        else:
            enter_text(n_pw)

        if n_site:
            enter_text(n_site)
        else:
            press_select()

        if n_body:
            enter_text(n_body, multiline=True)
        else:
            press_cancel()

        # view
        time.sleep(0.1)
        m = cap_menu()
        N = 1
        assert m[0] == f'"{n_title}"'
        if n_user and not n_site:
            assert n_user in m[1]
            N += 1
        elif n_site and not n_user:
            assert n_site in m[1]
            N += 1
        elif n_site and n_user:
            assert n_user in m[1]
            assert n_site in m[2]
            N += 2

        assert 'View Password' in m
        assert 'Send Password' in m
        assert 'Export' in m
        assert 'Edit Metadata' in m
        assert 'Delete' in m
        assert 'Change Password' in m

        # top 3 menu items do same thing: view details
        for idx in range(N):
            pick_menu_item(m[idx])
            title, story = cap_story()
            assert title == n_title
            if n_user:
                assert f'User: {n_user}' in story
            if n_site:
                assert f'Site: {n_site}' in story
            assert 'Password: (' in story
            if n_body:
                assert 'Notes:' in story
                assert story.endswith(n_body)

            need_keypress(KEY_CANCEL)

        # view pw as text and QR
        pick_menu_item('View Password')
        title, story = cap_story()
        assert title == n_title
        assert story == n_pw

        if not is_headless:
            need_keypress(KEY_QR)
            qr_rb = cap_screen_qr().decode('utf-8')
            assert qr_rb == n_pw
            need_keypress(KEY_CANCEL)

    return doit


@pytest.fixture
def change_password(goto_notes, pick_menu_item, enter_text, cap_story,
                    need_keypress, settings_get, press_select, press_cancel,
                    cap_menu):

    def doit(id_title, new_title=None, new_username=None, new_site=None,
             new_misc=None, new_password=None, old_password=None):
        goto_notes()
        m = cap_menu()
        found = [i for i in m if f': {id_title}' in i]
        assert found

        pick_menu_item(found[0])

        if new_title or new_username or new_site or new_misc:
            need_in_story = []
            pick_menu_item('Edit Metadata')
            if new_title:
                enter_text(KEY_CLEAR + new_title)
                need_in_story.append('Title')
            else:
                press_select()
            if new_username:
                enter_text(KEY_CLEAR + new_username)
                need_in_story.append('Username')
            else:
                press_select()
            if new_site:
                enter_text(KEY_CLEAR + new_site)
                need_in_story.append('Site Name')
            else:
                press_select()
            if new_misc:
                enter_text(KEY_CLEAR + new_misc, multiline=True)
                need_in_story.append('Other Notes')
            else:
                press_cancel()

            # approve change
            time.sleep(0.1)
            title, story = cap_story()
            assert 'SURE' in title
            for i in need_in_story:
                assert i in story
            need_keypress(KEY_ENTER)

        if new_password:
            pick_menu_item('Change Password')
            enter_text(KEY_CLEAR + new_password)

            # confirm
            time.sleep(0.1)
            title, story = cap_story()
            assert 'Confirm' in title
            assert 'New Password' in story
            assert 'Old Password' in story
            assert new_password in story

            need_keypress(KEY_ENTER)

        # test changes at low-level
        time.sleep(0.1)
        notes = settings_get('notes')
        note = [n for n in notes if n['title'] == (new_title or id_title)][0]
        assert note
        if new_site:
            assert note['site'] == new_site
        if new_username:
            assert note['user'] == new_username
        if new_misc:
            assert note['misc'] == new_misc
        if new_password:
            assert note['password'] == new_password

    return doit


@pytest.mark.parametrize('n_title', [ 'a', 'aaa', 'b'*32])
@pytest.mark.parametrize('n_body', [ 'short', 'very long '*30, 'mOKa', 'x X x'])
def test_build_note(n_title, n_body, build_note, delete_note):
    build_note(n_title, n_body)
    delete_note(n_title)

    
@pytest.mark.parametrize('size', [ 4000, 30000])
@pytest.mark.parametrize('encoding', '2Z' )
def test_huge_notes(size, encoding, goto_notes, enter_text, cap_menu, need_keypress,
                    scan_a_qr, settings_set, settings_get):

    # Since we don't limit note sizes, by request of NVK ... test them
    
    n_body = ''.join(chr((i%95) + 32) for i in prandom(size))
    n_title = f'Size {size} {random.randint(100000, 999999)}'

    # kill old things, enable feature
    settings_set('notes', [])

    goto_notes('New Note')
    enter_text(n_title)

    # use BBRq to import body -- fast and verbatim
    need_keypress(KEY_QR)

    actual_vers, parts = split_qrs(n_body, 'U',  max_version=20, encoding=encoding)
    random.shuffle(parts)

    for p in parts:
        scan_a_qr(p)
        time.sleep(2.0 / len(parts))       # just so we can watch
    
    time.sleep(.5)      # decompression time in some cases
    m = cap_menu()
    assert m[-2] == 'Export'

    notes = settings_get('notes')
    assert len(notes) == 1
    assert notes[0]['title'] == n_title
    assert notes[0]['misc'] == n_body

    settings_set('notes', [])
    goto_notes()        # redraw


@pytest.mark.parametrize('key', [None, KEY_QR])
@pytest.mark.parametrize('site', ["https://feed.org", None])
@pytest.mark.parametrize('user', [None, "joe"])
@pytest.mark.parametrize('misc', [None, "bla bla bla bla"])
def test_password_flow(key, site, user, misc, build_password, change_password):
    # Test password entry, including all the auto-generation capabilities
    title = os.urandom(4).hex()
    build_password(n_title=title, n_user=user, n_pw='A'*99,
                   n_site=site, n_body=misc,
                   key_pw=None)
    change_password(id_title=title,
                    new_username="changed" if user is None else None,
                    new_site="https://changed.org" if site is None else None,
                    new_misc="new bla newer bla newest bla" if misc is None else None)


@pytest.mark.parametrize('key', [KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5])
def test_password_flow_gen(key, build_password, change_password):
    title = os.urandom(4).hex()
    build_password(n_title=title, n_pw='B'*3, key_pw=key)
    change_password(id_title=title, new_password="changed")


def test_password_change_title(build_password, change_password):
    build_password(n_title="old_title", n_pw="default")
    change_password(id_title="old_title", new_title="new_title")


def test_top_export(goto_notes, pick_menu_item, cap_story, need_keypress, settings_get,
                    readback_bbqr, need_some_notes):

    notes = settings_get('notes', [])
    if not len(notes):
        notes = need_some_notes()

    goto_notes()
    pick_menu_item('Export All')

    title, story = cap_story()
    assert 'Export' in title
    assert 'to SD Card' in story
    assert 'to show QR' in story
    assert 'WARNING' in story
    assert 'will be cleartext' in story

    need_keypress(KEY_QR)
    file_type, data = readback_bbqr()
    assert file_type == 'J'
    obj = json.loads(data)
    assert obj.keys() == {'coldcard_notes'}
    assert obj['coldcard_notes'] == notes
    need_keypress(KEY_ENTER)

def test_sort_by_title(goto_notes, pick_menu_item, cap_story, need_keypress, settings_get,
                    settings_set, build_note, cap_menu, build_password):

    settings_set('notes', [])

    build_note('ZZZ', 'b1')

    goto_notes()
    assert 'Sort By Title' not in cap_menu()

    build_note('MMM', 'b2')
    build_note('AAA', 'b3')
    build_note('mmm', 'b2')
    build_note('Aaa', 'b3')
    build_password('Bbb')

    notes = settings_get('notes')

    goto_notes()
    pick_menu_item('Sort By Title')

    # effect is immedate
    after = settings_get('notes', [])

    assert sorted((i['title'] for i in after), key=lambda i:i.lower()) \
                    == [i['title'] for i in after]

def test_top_import(goto_notes, cap_menu, cap_story, need_keypress, settings_get,
                    settings_set, scan_a_qr, need_some_notes):
    # make some
    notes = need_some_notes()

    # wipe them
    settings_set('notes', [])

    goto_notes('Import')
    title, story = cap_story()
    assert 'Import' in title
    assert 'from SD Card' in story
    assert 'to scan QR' in story
    assert 'WARNING' not in story

    jj = json.dumps(dict(coldcard_notes=notes))

    need_keypress(KEY_QR)

    _, parts = split_qrs(jj, 'J', max_version=20)
    random.shuffle(parts)

    for p in parts:
        scan_a_qr(p)

    time.sleep(.5)  # decompression time in some cases
    m = cap_menu()
    for _ in range(3):
        if "1:" in m[0]:
            break
        time.sleep(.2)
        m = cap_menu()
        continue

    mm = [n.split(":")[-1].strip() for n in m if ":" in n]
    for note in notes:
        assert note['title'] in mm
    assert settings_get('notes', 'MISSING') == notes
    goto_notes()


@pytest.mark.parametrize('qr,title', [
    ('otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30',
     'ACME Co:john.doe@email.com'),
    ('otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi',
     'pi@raspberrypi'),
    ('otpauth-migration://offline?data=CiAKCghCEIa1rWta1rUSDEV4YW1wbGUgRGF0YSABKAEwAhAB',
     'Google Auth'),
])  
def test_top_qr(qr, title, goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress,
                settings_get, settings_set, scan_a_qr):
    # import some fun QR codes (will be notes) from top-level, undocumented
    goto_notes()
    need_keypress(KEY_QR)

    scan_a_qr(qr)
    time.sleep(1)

    # lazy readback
    notes = settings_get('notes', [])

    assert notes[-1]['title'] == title
    assert notes[-1]['misc'] == qr

    #pick_menu_item('Delete')
    #need_keypress(KEY_ENTER)


def test_top_disable(goto_notes, pick_menu_item, cap_menu, settings_get, settings_set):
    # Keep last - disables, deletes notes
    settings_set('notes', [])
    goto_notes()
    m = cap_menu()
    assert 'Disable Feature' in m
    pick_menu_item('Disable Feature')
    m = cap_menu()

    assert 'Ready To Sign' in m
    assert settings_get('notes', 'MISSING') == 'MISSING'


def test_tmp_notes_separation(goto_notes, pick_menu_item, generate_ephemeral_words,
                              build_note, build_password, seed_vault_enable, cap_menu,
                              restore_main_seed, press_select, goto_home):
    seed_vault_enable()
    goto_notes()
    # create some notes in master settings
    build_note(n_title="note-master", n_body="Master seed note meta")
    build_password(n_title="pwd-master", n_user="ccu", n_pw="fdshjd76342gdhj",
                   n_body="WIF: 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")

    # switch to random ephemeral seed
    generate_ephemeral_words(12, from_main=True, seed_vault=True)

    time.sleep(.1)
    goto_notes()
    m = cap_menu()
    assert len(m) == 4  # EMPTY - no saved notes

    build_note(n_title="note-tmp", n_body="Temporary seed note meta")
    build_password(n_title="pwd-tmp", n_user="ttu", n_pw="n7c4tvb6erdgg8",
                   n_body="HEX: 800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D")

    # switch to yet another random ephemeral seed
    generate_ephemeral_words(24, from_main=False, seed_vault=True)

    time.sleep(.1)
    goto_notes()
    m = cap_menu()
    assert len(m) == 4  # EMPTY - no saved notes

    build_note(n_title="note-tmp2", n_body="Second Temporary seed note meta")

    # back to master
    restore_main_seed(seed_vault=True)
    time.sleep(.1)

    goto_notes()
    m = cap_menu()
    mm = [n.split(":")[-1].strip() for n in m if ":" in n]
    assert 'note-master' in mm
    assert 'pwd-master' in mm
    assert 'note-tmp' not in mm
    assert 'pwd-tmp' not in mm
    assert 'note-tmp2' not in mm

    goto_home()
    pick_menu_item("Seed Vault")
    time.sleep(.1)
    m = cap_menu()

    # first tmp
    pick_menu_item(m[0])
    pick_menu_item("Use This Seed")
    press_select()
    goto_notes()
    m = cap_menu()
    mm = [n.split(":")[-1].strip() for n in m if ":" in n]
    assert 'note-master' not in mm
    assert 'pwd-master' not in mm
    assert 'note-tmp' in mm
    assert 'pwd-tmp' in mm
    assert 'note-tmp2' not in mm

    goto_home()
    pick_menu_item("Seed Vault")
    time.sleep(.1)
    m = cap_menu()

    # second tmp (directly from first tmp)
    pick_menu_item(m[1])
    pick_menu_item("Use This Seed")
    press_select()
    goto_notes()

    m = cap_menu()
    mm = [n.split(":")[-1].strip() for n in m if ":" in n]
    assert 'note-master' not in mm
    assert 'pwd-master' not in mm
    assert 'note-tmp' not in mm
    assert 'pwd-tmp' not in mm
    assert 'note-tmp2' in mm

    # back to master (again)
    restore_main_seed(seed_vault=True)
    time.sleep(.1)

    goto_notes()
    m = cap_menu()
    mm = [n.split(":")[-1].strip() for n in m if ":" in n]
    assert 'note-master' in mm
    assert 'pwd-master' in mm
    assert 'note-tmp' not in mm
    assert 'pwd-tmp' not in mm
    assert 'note-tmp2' not in mm


@pytest.mark.parametrize("msg", ["COLDCARD rocks!", "cc\nCC"])
@pytest.mark.parametrize("addr_fmt", [AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH])
@pytest.mark.parametrize("acct", [None, 0, 9999])
@pytest.mark.parametrize("way", ["sd", "qr", "nfc", "vdisk"])
def test_sign_note_body(msg, addr_fmt, acct, need_some_notes,
                        pick_menu_item, sign_msg_from_text, way,
                        goto_notes, settings_set):
    settings_set("notes", [])
    title = "aaa"
    need_some_notes(title, msg)
    goto_notes()
    pick_menu_item(f"1: {title}")
    pick_menu_item("Sign Note Text")
    sign_msg_from_text(msg, addr_fmt, acct, False, 0, way)


@pytest.mark.parametrize("chain", ["BTC", "XTN"])
@pytest.mark.parametrize("change", [True, False])
@pytest.mark.parametrize("idx", [None, 0, 9999])
def test_sign_password_free_form(chain, change, idx, need_some_passwords, settings_set,
                                 goto_notes, pick_menu_item, sign_msg_from_text):
    settings_set('notes', [])  # clear
    title = "A"
    msg = 'More Notes AAAA'
    settings_set('notes', [
        {'misc': msg,
         'password': 'fds65fd5f1sd51s',
         'site': 'https://a.com',
         'title': title,
         'user': 'AAA'}
    ])
    goto_notes()
    pick_menu_item(f"1: {title}")
    pick_menu_item("Sign Note Text")
    sign_msg_from_text(msg, AF_P2WPKH, None, change, idx, "qr", chain)

# EOF
