# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Various firmware upgrade things.
#
import pytest, os, struct, time, hashlib, subprocess
from sigheader import *
from ckcc_protocol.protocol import MAX_MSG_LEN, CCProtocolPacker, CCProtoError
from collections import namedtuple

Header = namedtuple('Header', FWH_PY_VALUES)
packed_len = struct.calcsize(FWH_PY_FORMAT)
assert packed_len == FW_HEADER_SIZE

def parse_hdr(hdr):
    return Header(**dict(zip(FWH_PY_VALUES.split(), struct.unpack(FWH_PY_FORMAT, hdr))))

@pytest.fixture
def upload_file(dev):
    def doit(data, pkt_len=2048):
        for pos in range(0, len(data), pkt_len):
            v = dev.send_recv(CCProtocolPacker.upload(pos, len(data), data[pos:pos+pkt_len]))
            assert v == pos
            chk = dev.send_recv(CCProtocolPacker.sha256())
            assert chk == hashlib.sha256(data[0:pos+pkt_len]).digest(), 'bad hash'
    return doit

@pytest.fixture
def make_firmware(src_root_dir):
    def doit(hw_compat, fname=f'{src_root_dir}/stm32/firmware-signed.bin', outname='tmp-firmware.bin'):
        # os.system(f'signit sign 3.0.99 --keydir ../stm32/keys -r {fname} -o {outname} --hw-compat=0x{hw_compat:02x}')
        p = subprocess.run(
            [
                'signit', 'sign', '3.0.99',
                 '--keydir', f'{src_root_dir}/stm32/keys',
                 '-r', f'{fname}',
                 '-o', f'{outname}',
                 f'--hw-compat={hw_compat}'
            ],
            capture_output=True,
            text=True,
        )
        if p.stderr:
            raise RuntimeError(p.stderr)

        rv = open(outname, 'rb').read()

        os.unlink(outname)

        return rv
    return doit

@pytest.fixture
def upgrade_by_sd(open_microsd, cap_story, pick_menu_item, goto_home, press_select, microsd_path, sim_exec, src_root_dir):

    # send a firmware file over the microSD card

    def doit(data, expect_fail=None):

        fname = 'tmp-firmware'

        # stop it from reseting at end of process
        sim_exec('import machine; machine.reset = lambda:None')

        # create DFU file (wrapper)
        open(f'{fname}.bin', 'wb').write(data)
        dfu = microsd_path('tmp-firmware.dfu')
        cmd = f'{src_root_dir}/external/micropython/tools/dfu.py -b 0x08008000:{fname}.bin {dfu}'
        print(cmd)
        os.system(cmd)

        goto_home()
        pick_menu_item('Advanced/Tools')
        try:
            pick_menu_item('Upgrade')
        except KeyError:
            pick_menu_item('Upgrade Firmware')
        pick_menu_item('From MicroSD')

        time.sleep(.1)
            
        pick_menu_item(os.path.basename(dfu))

        if expect_fail:
            time.sleep(2)
            title, story = cap_story()
            assert title == 'Sorry!'
            assert expect_fail in story

    return doit


@pytest.mark.parametrize('mode', ['compat', 'incompat'])
@pytest.mark.parametrize('transport', ['sd', 'usb'])
def test_hacky_upgrade(mode, cap_story, transport, dev, sim_exec, make_firmware, upload_file,
                       upgrade_by_sd, press_cancel, is_q1, is_mark5):

    if mode == 'compat':
        data = make_firmware("q1" if is_q1 else (5 if is_mark5 else 4))
    elif mode == 'incompat':
        data = make_firmware(4 if is_q1 else "q1")

    hdr = data[FW_HEADER_OFFSET:FW_HEADER_OFFSET+FW_HEADER_SIZE]

    cooked = parse_hdr(hdr)
    #print(cooked)
    assert cooked.magic_value == FW_HEADER_MAGIC
    assert cooked.firmware_length == len(data)

    if mode == 'incompat':
        if transport == 'usb':
            with pytest.raises(CCProtoError) as ee:
                upload_file(data + hdr)
            assert "doesn't support this version of Coldcard" in str(ee)
        else:
            upgrade_by_sd(data, expect_fail="doesn't support this version of Coldcard")

        return

    # file should be accepted
    if transport == 'usb':
        upload_file(data + hdr)
    else:
        upgrade_by_sd(data)

    _, story = cap_story()
    assert "Install this new firmware?" in story
    press_cancel()
    # check data was uploaded verbatim (VERY SLOW)
    # for pos in range(0, cooked.firmware_length + 128, 128):
    #     to_eval = f'from sflash import SF;SF.array[{pos}:{pos+128}]'
    #     x = sim_exec(to_eval)
    #     a = eval(x)
    #     if pos in [ FW_HEADER_OFFSET, cooked.firmware_length]:
    #         assert a == hdr, f"wrong @ {pos}"
    #     else:
    #         assert a == data[pos:pos+128], repr(pos)


# EOF
