# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# A few USB link layer tests.
#
import pytest, time, struct
from pycoin.key.BIP32Node import BIP32Node
from binascii import b2a_hex, a2b_hex
from ckcc_protocol.protocol import MAX_MSG_LEN, CCProtocolPacker, CCProtoError

def test_usb_fuzz(dev):
    # test framing logic
    # - expect a few console errors

    def llread():
        # unify unix socket vs. USB pipe differences
        rv = dev.dev.read(64, timeout_ms=100)
        if rv == None: return
        return bytes(rv) or None

    # do-nothing msg
    dev.dev.write(b'\x80' + (b'\x01'*63))
    resp = llread()
    assert resp == None, repr(resp)

    if 0:
        # leverage bug(?) in HIDapi to test short EP writes
        dev.dev.write(b'\x00'*64)
        resp = llread()
        assert resp[1:].startswith(b'framshort'), resp

    # get out of sync. and recover
    dev.dev.write(b'\x3f' + b'ping'+ (b'a'*(64-4-1)))
    resp = llread()
    assert resp == None
    dev.dev.write(b'\x00' + b'ping'+ (b'b'*(64-4-1)))
    assert resp == None
    resp = llread()

    dev.dev.write(bytes([0x80 + 0x3f]) +b'ping'+ (b'-'*(64-4-1)))
    resp = llread()
    assert resp[1:] == b'biny'+(b'-' * (0x3f-4)), resp

    # various length junk messages (single packet)
    for n in [1, 2, 3, 4, 5, 50, 63]:
        dev.dev.write(bytes([n | 0x80]) + b'abcd' + bytes(64-4-1)) 
        resp = llread()
        msg = resp[1:1+(resp[0] & 0x3f)]
        print("Bad length test: %2d => %r" % (n, msg.decode('ascii')))
        if n < 4:
            assert msg[0:4] == b'fram', repr(resp)
        else:
            assert msg[0:4] == b'err_', repr(resp)

    # too long
    print("Long msg test, start.")
    for n in range(2000):
        dev.dev.write(b'\x3f' + b'\xff' + bytes(62))
        resp = llread()
        if resp == None: continue
        print("stopped @ %d msgs" % n)
        assert resp[1:1+4] == b'fram', resp
        break
    

# note: 0x80000000 = 2147483648

@pytest.mark.parametrize('path', [ 'm', 'm/', 'm/1', "m/1'", "m/1'/0/1'",
    "m/2147483647", "m/2147483647'",
    'm/1/2/3/4/5/6/7/8/9/10'])
def test_xpub_good(dev, master_xpub, path):
    # get some xpubs and validate the derivations

    xpub = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)

    assert xpub[1:4] == 'pub'
    assert len(xpub) > 100

    # check the derive using pycoin
    k = BIP32Node.from_wallet_key(xpub)

    assert k.hwif() == xpub

    if "'" not in path:
        mk = BIP32Node.from_wallet_key(master_xpub)
        sk = mk.subkey_for_path(path[2:])
        assert sk.hwif() == xpub

    if len(path) <= 2:
        assert mk.fingerprint() == struct.pack('<I', dev.master_fingerprint)

@pytest.mark.parametrize('path', [ '', '1/2', 'x/1/2', "m'"])
def test_xpub_invalid(dev, path):
    # some bad paths

    with pytest.raises(CCProtoError):
        xpub = dev.send_recv(CCProtocolPacker.get_xpub(path), timeout=None)
    

def test_version(dev):
    # read the version, yawn.
    v = dev.send_recv(CCProtocolPacker.version())
    assert '\n' in v
    date, label, bl, *extras = v.split('\n')
    assert '-' in date
    assert '.' in label
    assert '.' in bl
    print("date=%s" % date)

@pytest.mark.parametrize('data_len', [1, 24, 60, 61, 62, 63, 64, 1000])
def test_upload_short(dev, data_len):
    # upload a few really short files
    
    from hashlib import sha256

    data = b'a'*data_len

    v = dev.send_recv(CCProtocolPacker.upload(0, len(data), data))
    assert v == 0
    chk = dev.send_recv(CCProtocolPacker.sha256())

    assert chk == sha256(data).digest(), 'bad hash'

    # clear screen / test a degerate case
    dev.send_recv(CCProtocolPacker.upload(256, 256, b''))

@pytest.mark.parametrize('pkt_len', [256, 1024, 2048])
def test_upload_long(dev, pkt_len, count=5):
    # upload a larger "file"
    
    from hashlib import sha256
    import os

    data = os.urandom(pkt_len * count)

    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 == sha256(data[0:pos+pkt_len]).digest(), 'bad hash'

    # clear screen / test a degerate case
    dev.send_recv(CCProtocolPacker.upload(256, 256, b''))

def test_upload_fails(dev):
    # incorrect file upload cases

    data = b'3'*60

    with pytest.raises(CCProtoError):
        # misaligned
        v = dev.send_recv(CCProtocolPacker.upload(23, 23, data))

    with pytest.raises(CCProtoError):
        # bad position
        v = dev.send_recv(CCProtocolPacker.upload(1000, 3, data))

def test_encryption(dev):
    "Setup session key and test link encryption works"

    #dev = ColdcardDevice(sn=force_serial, encrypt=False)
    #dev.start_encryption()

    print("Session key: " + str(b2a_hex(dev.session_key), 'utf'))

    for blen in [4, 8, 60, 128, 256, MAX_MSG_LEN-4]:
        rb = dev.send_recv(CCProtocolPacker.ping(bytes(blen)), encrypt=1)
        assert set(rb) == {0} and len(rb) == blen

        rb = dev.send_recv(CCProtocolPacker.ping(bytes(blen)), encrypt=0)
        assert set(rb) == {0} and len(rb) == blen

    was = dev.session_key
    assert len(was) == 32
    assert len(set(was)) > 8

    # rekey
    dev.start_encryption()
    assert dev.session_key != was
    assert len(set(dev.session_key)) > 8

def test_mitm(dev):
    
    # simple check
    dev.check_mitm()

    # do again
    sig2 = dev.send_recv(CCProtocolPacker.check_mitm(), timeout=5000)
    
    old_key = dev.session_key
    dev.check_mitm(sig=sig2)

    dev.start_encryption()
    assert old_key != dev.session_key

    assert dev.mitm_verify(sig2, dev.master_xpub) == False

def test_remote_upload(dev):
    import os
    dev.upload_file(b'testing')
    dev.upload_file(os.urandom(3000))

@pytest.mark.parametrize('f_len', [256, 1024, 2048, 384*1024])
def test_remote_up_download(f_len, dev):
    import os
    data = os.urandom(f_len)
    ll, sha = dev.upload_file(data, verify=True)
    assert ll == len(data) == f_len

    rb = dev.download_file(ll, sha, file_number=0)
    assert rb == data

# EOF
