import pytest
from pathlib import Path
from typing import Callable
from ragger.error import ExceptionRAPDU
from ragger.firmware import Firmware
from ragger.backend import BackendInterface
from ragger.navigator import Navigator, NavInsID
from ledger_app_clients.ethereum.client import EthAppClient, TxData, StatusWord
from ledger_app_clients.ethereum.settings import SettingID, settings_toggle
from ledger_app_clients.ethereum.utils import get_selector_from_function
import struct


ROOT_SCREENSHOT_PATH = Path(__file__).parent

BIP32_PATH = "m/44'/60'/0'/0/0"
NONCE = 21
GAS_PRICE = 13
GAS_LIMIT = 21000
FROM = bytes.fromhex("1122334455667788990011223344556677889900")
TO = bytes.fromhex("0099887766554433221100998877665544332211")
NFTS = [ (1, 3), (5, 2), (7, 4) ] # tuples of (token_id, amount)
DATA = "Some data".encode()

class   NFTCollection:
    addr: bytes
    name: str
    chain_id: int
    def __init__(self, addr: bytes, name: str, chain_id: int):
        self.addr = addr
        self.name = name
        self.chain_id = chain_id

class   Action:
    fn: str
    data_fn: Callable
    nav_fn: Callable
    def __init__(self, fn: str, data_fn: Callable, nav_fn: Callable):
        self.fn = fn
        self.data_fn = data_fn
        self.nav_fn = nav_fn

def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool) -> list[NavInsID]:
    moves = list()
    if is_nano:
        moves += [ NavInsID.RIGHT_CLICK ] * nano_steps
        if reject:
            moves += [ NavInsID.RIGHT_CLICK ]
        moves += [ NavInsID.BOTH_CLICK ]
    else:
        moves += [ NavInsID.USE_CASE_REVIEW_TAP ] * stax_steps
        if reject:
            moves += [
                NavInsID.USE_CASE_REVIEW_REJECT,
                NavInsID.USE_CASE_CHOICE_CONFIRM
            ]
        else:
            moves += [ NavInsID.USE_CASE_REVIEW_CONFIRM ]
    return moves

def snapshot_test_name(nft_type: str, fn: str, chain_id: int, reject: bool) -> str:
    name = "%s_%s_%s" % (nft_type, fn.split("(")[0], str(chain_id))
    if reject:
        name += "-rejected"
    return name

def common_test_nft(fw: Firmware,
                    back: BackendInterface,
                    nav: Navigator,
                    collec: NFTCollection,
                    action: Action,
                    reject: bool,
                    plugin_name: str):
    app_client = EthAppClient(back)
    selector = get_selector_from_function(action.fn)

    if app_client._client.firmware.name == "nanos":
        pytest.skip("Not supported on LNS")
    with app_client.set_plugin(plugin_name,
                               collec.addr,
                               selector,
                               1):
        pass
    with app_client.provide_nft_metadata(collec.name, collec.addr, collec.chain_id):
        pass
    with app_client.sign_legacy(BIP32_PATH,
                                NONCE,
                                GAS_PRICE,
                                GAS_LIMIT,
                                collec.addr,
                                0,
                                collec.chain_id,
                                action.data_fn(action)):
        nav.navigate_and_compare(ROOT_SCREENSHOT_PATH,
                                 snapshot_test_name(plugin_name.lower(),
                                                    action.fn,
                                                    collec.chain_id,
                                                    reject),
                                 action.nav_fn(fw.is_nano,
                                               collec.chain_id,
                                               reject))

def common_test_nft_reject(test_fn: Callable,
                           fw: Firmware,
                           back: BackendInterface,
                           nav: Navigator,
                           collec: NFTCollection,
                           action: Action):
    try:
        test_fn(fw, back, nav, collec, action, True)
    except ExceptionRAPDU as e:
        assert e.status == StatusWord.CONDITION_NOT_SATISFIED
    else:
        assert False # An exception should have been raised

# ERC-721

ERC721_PLUGIN = "ERC721"
ERC721_SAFE_TRANSFER_FROM_DATA = "safeTransferFrom(address,address,uint256,bytes)"
ERC721_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256)"
ERC721_TRANSFER_FROM = "transferFrom(address,address,uint256)"
ERC721_APPROVE = "approve(address,uint256)"
ERC721_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)"

## data formatting functions

def data_erc721_transfer_from(action: Action) -> TxData:
    return TxData(
        get_selector_from_function(action.fn),
        [
            FROM,
            TO,
            struct.pack(">H", NFTS[0][0])
        ]
    )

def data_erc721_safe_transfer_from_data(action: Action) -> TxData:
    txd = data_erc721_transfer_from(action)
    txd.parameters += [ DATA ]
    return txd

def data_erc721_approve(action: Action) -> TxData:
    return TxData(
        get_selector_from_function(action.fn),
        [
            TO,
            struct.pack(">H", NFTS[0][0])
        ]
    )

def data_erc721_set_approval_for_all(action: Action) -> TxData:
    return TxData(
        get_selector_from_function(action.fn),
        [
            TO,
            struct.pack("b", False)
        ]
    )

## ui nav functions

def nav_erc721_transfer_from(is_nano: bool,
                             chain_id: int,
                             reject: bool) -> list[NavInsID]:
    nano_steps = 7
    stax_steps = 3
    if chain_id != 1:
        nano_steps += 1
        stax_steps += 1
    return common_nav_nft(is_nano, nano_steps, stax_steps, reject)

def nav_erc721_approve(is_nano: bool,
                       chain_id: int,
                       reject: bool) -> list[NavInsID]:
    nano_steps = 7
    stax_steps = 3
    if chain_id != 1:
        nano_steps += 1
        stax_steps += 1
    return common_nav_nft(is_nano, nano_steps, stax_steps, reject)

def nav_erc721_set_approval_for_all(is_nano: bool,
                                    chain_id: int,
                                    reject: bool) -> list[NavInsID]:
    nano_steps = 6
    if chain_id != 1:
        nano_steps += 1
    return common_nav_nft(is_nano, nano_steps, 3, reject)

collecs_721 = [
    NFTCollection(bytes.fromhex("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d"),
        "Bored Ape Yacht Club",
        1),
    NFTCollection(bytes.fromhex("670fd103b1a08628e9557cd66b87ded841115190"),
        "y00ts",
        137),
    NFTCollection(bytes.fromhex("2909cf13e458a576cdd9aab6bd6617051a92dacf"),
        "goerlirocks",
        5)
]
actions_721 = [
    Action(ERC721_SAFE_TRANSFER_FROM_DATA,
           data_erc721_safe_transfer_from_data,
           nav_erc721_transfer_from),
    Action(ERC721_SAFE_TRANSFER_FROM,
           data_erc721_transfer_from,
           nav_erc721_transfer_from),
    Action(ERC721_TRANSFER_FROM,
           data_erc721_transfer_from,
           nav_erc721_transfer_from),
    Action(ERC721_APPROVE,
           data_erc721_approve,
           nav_erc721_approve),
    Action(ERC721_SET_APPROVAL_FOR_ALL,
           data_erc721_set_approval_for_all,
           nav_erc721_set_approval_for_all)
]


@pytest.fixture(params=collecs_721)
def collec_721(request) -> NFTCollection:
    return request.param
@pytest.fixture(params=actions_721)
def action_721(request) -> Action:
    return request.param

def test_erc721(firmware: Firmware,
                 backend: BackendInterface,
                 navigator: Navigator,
                 collec_721: NFTCollection,
                 action_721: Action,
                 reject: bool = False):
    common_test_nft(firmware,
                    backend,
                    navigator,
                    collec_721,
                    action_721,
                    reject,
                    ERC721_PLUGIN)

def test_erc721_reject(firmware: Firmware,
                        backend: BackendInterface,
                        navigator: Navigator):
    common_test_nft_reject(test_erc721,
                           firmware,
                           backend,
                           navigator,
                           collecs_721[0],
                           actions_721[0])

# ERC-1155

ERC1155_PLUGIN = "ERC1155"
ERC1155_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256,uint256,bytes)"
ERC1155_SAFE_BATCH_TRANSFER_FROM = "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)"
ERC1155_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)"

## data formatting functions

def data_erc1155_safe_transfer_from(action: Action) -> TxData:
    return TxData(
        get_selector_from_function(action.fn),
        [
            FROM,
            TO,
            struct.pack(">H", NFTS[0][0]),
            struct.pack(">H", NFTS[0][1]),
            DATA
        ]
    )

def data_erc1155_safe_batch_transfer_from(action: Action) -> TxData:
    data = TxData(
        get_selector_from_function(action.fn),
        [
            FROM,
            TO
        ])
    data.parameters += [ int(32 * 4).to_bytes(8, "big") ] # token_ids offset
    data.parameters += [int(32 * (4 + len(NFTS) + 1)).to_bytes(8, "big") ] # amounts offset
    data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # token_ids length
    for nft in NFTS:
        data.parameters += [ struct.pack(">H", nft[0]) ] # token_id
    data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # amounts length
    for nft in NFTS:
        data.parameters += [ struct.pack(">H", nft[1]) ] # amount
    return data

def data_erc1155_set_approval_for_all(action: Action) -> TxData:
    return TxData(
        get_selector_from_function(action.fn),
        [
            TO,
            struct.pack("b", False)
        ]
    )

## ui nav functions

def nav_erc1155_safe_transfer_from(is_nano: bool,
                                   chain_id: int,
                                   reject: bool) -> list:
    nano_steps = 8
    if chain_id != 1:
        nano_steps += 1
    return common_nav_nft(is_nano, nano_steps, 4, reject)

def nav_erc1155_safe_batch_transfer_from(is_nano: bool,
                                         chain_id: int,
                                         reject: bool) -> list:
    nano_steps = 7
    stax_steps = 3
    if chain_id != 1:
        nano_steps += 1
        stax_steps += 1
    return common_nav_nft(is_nano, nano_steps, stax_steps, reject)

def nav_erc1155_set_approval_for_all(is_nano: bool,
                                     chain_id: int,
                                     reject: bool) -> list:
    nano_steps = 6
    if chain_id != 1:
        nano_steps += 1
    return common_nav_nft(is_nano, nano_steps, 3, reject)

collecs_1155 = [
    NFTCollection(bytes.fromhex("495f947276749ce646f68ac8c248420045cb7b5e"),
        "OpenSea Shared Storefront",
        1),
    NFTCollection(bytes.fromhex("2953399124f0cbb46d2cbacd8a89cf0599974963"),
        "OpenSea Collections",
        137),
    NFTCollection(bytes.fromhex("f4910c763ed4e47a585e2d34baa9a4b611ae448c"),
        "OpenSea Collections",
        5)
]
actions_1155 = [
    Action(ERC1155_SAFE_TRANSFER_FROM,
           data_erc1155_safe_transfer_from,
           nav_erc1155_safe_transfer_from),
    Action(ERC1155_SAFE_BATCH_TRANSFER_FROM,
           data_erc1155_safe_batch_transfer_from,
           nav_erc1155_safe_batch_transfer_from),
    Action(ERC1155_SET_APPROVAL_FOR_ALL,
           data_erc1155_set_approval_for_all,
           nav_erc1155_set_approval_for_all)
]
@pytest.fixture(params=collecs_1155)
def collec_1155(request) -> bool:
    return request.param
@pytest.fixture(params=actions_1155)
def action_1155(request) -> Action:
    return request.param

def test_erc1155(firmware: Firmware,
                 backend: BackendInterface,
                 navigator: Navigator,
                 collec_1155: NFTCollection,
                 action_1155: Action,
                 reject: bool = False):
    common_test_nft(firmware,
                    backend,
                    navigator,
                    collec_1155,
                    action_1155,
                    reject,
                    ERC1155_PLUGIN)

def test_erc1155_reject(firmware: Firmware,
                        backend: BackendInterface,
                        navigator: Navigator):
    common_test_nft_reject(test_erc1155,
                           firmware,
                           backend,
                           navigator,
                           collecs_1155[0],
                           actions_1155[0])
