# click-test generator
#
# Beware: This tool is alpha-stage, both in terms of functionality and code quality.
# It is published here because it can be useful as-is. But it's full of bugs in any
# case.
#
# See docs/tests/click-tests.md for a brief instruction manual.

import inspect
import sys
import threading
import time
from pathlib import Path

import click

from trezorlib import (
    binance,
    btc,
    cardano,
    cosi,
    device,
    eos,
    ethereum,
    fido,
    firmware,
    misc,
    monero,
    nem,
    ripple,
    stellar,
    tezos,
)
from trezorlib.cli.trezorctl import cli as main

from trezorlib import cli, debuglink, protobuf  # isort:skip


# make /tests part of sys.path so that we can import buttons.py as a module
ROOT = Path(__file__).resolve().parent.parent
sys.path.append(str(ROOT))
import buttons  # isort:skip

MODULES = (
    binance,
    btc,
    cardano,
    cosi,
    device,
    eos,
    ethereum,
    fido,
    firmware,
    misc,
    monero,
    nem,
    ripple,
    stellar,
    tezos,
)


CALLS_DONE = []
DEBUGLINK = None

get_client_orig = cli.TrezorConnection.get_client


def get_client(conn):
    global DEBUGLINK
    client = get_client_orig(conn)
    DEBUGLINK = debuglink.DebugLink(client.transport.find_debug())
    DEBUGLINK.open()
    DEBUGLINK.watch_layout(True)
    return client


cli.TrezorConnection.get_client = get_client


def scan_layouts(dest):
    while True:
        try:
            layout = DEBUGLINK.wait_layout()
        except Exception:
            return
        dest.append(layout)


CLICKS_HELP = """\
Type 'y' or just press Enter to click the right button (usually "OK")
Type 'n' to click the left button (usually "Cancel")
Type a digit (0-9) to click the appropriate button as if on a numpad.
Type 'g1,2' to click button in column 1 and row 2 of 3x5 grid (letter A on mnemonic keyboard).
Type 'i 1234' to send text "1234" without clicking (useful for PIN, passphrase, etc.)
Type 'u' or 'j' to swipe up, 'd' or 'k' to swipe down.
Type 'confirm' for hold-to-confirm (or a confirmation signal without clicking).
Type 'stop' to stop recording.
"""


def layout_to_output(layout):
    if isinstance(layout, str):
        return layout

    if len(layout.lines) > 1:
        text = " ".join(layout.lines[1:])
        return f"assert {text!r} in layout.text"
    else:
        return f"assert layout.text == {layout.text!r}"


def echo(what):
    click.secho(what, fg="blue", err=True)
    sys.stderr.flush()


def send_clicks(dest):
    echo(CLICKS_HELP)
    while True:
        key = click.prompt(click.style("Send click", fg="blue"), default="y")
        sys.stderr.flush()

        try:
            layout = DEBUGLINK.read_layout()
            echo("Please wait...")

            if key == "confirm":
                output = "debug.input(button=messages.DebugButton.YES)"
                DEBUGLINK.press_yes()
            elif key in "uj":
                output = "debug.input(swipe=messages.DebugSwipeDirection.UP)"
                DEBUGLINK.swipe_up()
            elif key in "dk":
                output = "debug.input(swipe=messages.DebugSwipeDirection.DOWN)"
                DEBUGLINK.swipe_down()
            elif key.startswith("i "):
                input_str = key[2:]
                output = f"debug.input({input_str!r})"
                DEBUGLINK.input(input_str)
            elif key.startswith("g"):
                x, y = [int(s) - 1 for s in key[1:].split(",")]
                output = f"debug.click(buttons.grid35({x}, {y}))"
                DEBUGLINK.click(buttons.grid35(x, y))
            elif key == "y":
                output = "debug.click(buttons.OK)"
                DEBUGLINK.click(buttons.OK)
            elif key == "n":
                output = "debug.click(buttons.CANCEL)"
                DEBUGLINK.click(buttons.CANCEL)
            elif key in "0123456789":
                if key == "0":
                    x, y = 1, 4
                else:
                    i = int(key) - 1
                    x = i % 3
                    y = 3 - (i // 3)  # trust me
                output = f"debug.click(buttons.grid35({x}, {y}))"
                DEBUGLINK.click(buttons.grid35(x, y))
            elif key == "stop":
                return
            else:
                raise Exception

            # give emulator time to react
            time.sleep(0.5)
            new_layout = DEBUGLINK.read_layout()
            if new_layout != layout:
                # assume emulator reacted with a layout change
                output = f"layout = {output[:-1]}, wait=True)"

            dest.append(output)
        except Exception:
            echo("bad input")


def record_wrapper(name, func):
    def wrapper(*args, **kwargs):
        clicks = []
        layouts = []
        call_item = [name, args, kwargs, clicks, layouts]

        clicks_thr = threading.Thread(target=send_clicks, args=(clicks,), daemon=True)
        clicks_thr.start()
        try:
            result = func(*args, **kwargs)
            call_item.extend([result, None])
            return result
        except BaseException as e:
            call_item.extend([None, e])
            raise
        finally:
            clicks_thr.join()

            thr = threading.Thread(target=scan_layouts, args=(layouts,), daemon=True)
            thr.start()
            # 5 seconds to download all layout changes so far should be enough
            thr.join(timeout=5)

            DEBUGLINK.close()  # this should shut down the layout scanning thread
            thr.join()
            DEBUGLINK.open()

            CALLS_DONE.append(call_item)

    return wrapper


for module in MODULES:
    assert inspect.ismodule(module)
    for name, member in inspect.getmembers(module):
        if not inspect.isfunction(member):
            continue

        sig = inspect.signature(member)
        params = list(sig.parameters)
        if params[0] != "client":
            continue

        func_name = f"{module.__name__}.{name}"
        setattr(module, name, record_wrapper(func_name, member))


def call_to_strs(call):
    func_name, args, kwargs, clicks, layouts, result, exception = call
    args = args[1:]

    arg_text = [repr(arg) for arg in args]
    arg_text.extend(f"{name}={value!r}" for name, value in kwargs.items())

    call = f"device_handler.run({func_name}, {', '.join(arg_text)})"

    result_list = []
    if exception is None:
        if isinstance(result, protobuf.MessageType):
            result_list.append("result = device_handler.result()")
            result_list.append(
                f"assert isinstance(result, messages.{result.__class__.__name__}"
            )
            for k, v in result.__dict__.items():
                if v is None or v == []:
                    continue
                result_list.append(f"assert result.{k} == {v!r}")
        else:
            result_list.append(f"assert device_handler.result() == {result!r}")
    else:
        result_list = [
            f"with pytest.raises({exception.__class__.__name__}):",
            "    device_handler.result()",
        ]

    layouts_iter = iter(layouts)
    layout_text = ["layout = debug.wait_layout()", next(layouts_iter)]

    for instr in clicks:
        layout_text.append(instr)
        if "wait=True" in instr:
            layout_text.append(next(layouts_iter))

    # finish layouts iterator -- should not do anything ideally
    layout_text.extend(layouts_iter)

    layout_text = [layout_to_output(l) for l in layout_text]

    all_lines = "\n".join([call] + layout_text + result_list).split("\n")
    all_lines = [f"    {l}" for l in all_lines]
    func_name_under = func_name.replace(".", "_")
    all_lines.insert(0, f"def test_{func_name_under}(device_handler):")
    all_lines.insert(1, "    debug = device_handler.debuglink()")

    return "\n".join(all_lines)


if __name__ == "__main__":
    echo(
        """\
Quick&Dirty Test Case Recorder.

Use as you would use trezorctl, input clicking commands via host keyboard
for best results.
"""
    )
    try:
        main()
    finally:
        if DEBUGLINK is not None:
            DEBUGLINK.watch_layout(False)
            DEBUGLINK.close()
            echo("\n# ========== test cases ==========\n")
            for call in CALLS_DONE:
                testcase = call_to_strs(call)
                echo(testcase)
                echo("\n")
