# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# needs local bitcoind in PATH

import os, time, uuid, socket, shutil, pytest, tempfile, subprocess, signal, base64
from authproxy import AuthServiceProxy, JSONRPCException
from helpers import xfp2str
from ckcc.protocol import CCProtocolPacker


def find_bitcoind():
    # search for the binary we need
    # - should be in the path really
    env_path = os.environ.get("CC_TEST_BITCOIND", None)
    if env_path:
        return env_path

    easy = shutil.which('bitcoind')
    if easy:
        return easy
    
    # - default landing spot for MacOS .dmg from bitcoin.org
    mac_default = '/Applications/Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt'
    if os.path.exists(mac_default):
        return mac_default

    raise RuntimeError("Need a binary for bitcoin core. Check path?")


# stolen from HWI test suite and slightly modified
class Bitcoind:
    def __init__(self):
        self.bitcoind_path = find_bitcoind()
        self.datadir = tempfile.mkdtemp()
        self.rpc = None
        self.bitcoind_proc = None
        self.userpass = None
        self.supply_wallet = None
        self.has_bdb = True
        self.version = None

    def start(self):

        def get_free_port():
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.bind(("", 0))
            s.listen(1)
            port = s.getsockname()[1]
            s.close()
            return port

        self.p2p_port = get_free_port()
        self.rpc_port = get_free_port()

        self.bitcoind_proc = subprocess.Popen(
            [
                self.bitcoind_path,
                # needed for newest master
                # legacy wallet was deprecated in v29
                # and removed completely in v30
                "-deprecatedrpc=create_bdb",
                "-regtest",
                f"-datadir={self.datadir}",
                "-noprinttoconsole",
                "-fallbackfee=0.0002",
                "-server=1",
                "-listen=0",
                "-keypool=1",
                f"-port={self.p2p_port}",
                f"-rpcport={self.rpc_port}"
            ]
        )
        signal.signal(signal.SIGTERM, self.cleanup)

        # Wait for cookie file to be created
        cookie_path = os.path.join(self.datadir, "regtest", ".cookie")
        for i in range(20):
            if os.path.exists(cookie_path):
                break
            time.sleep(0.5)
        else:
            RuntimeError("'.cookie' not found. Is bitcoind running?")
        # Read .cookie file to get user and pass
        with open(cookie_path) as f:
            self.userpass = f.readline().lstrip().rstrip()
        self.rpc_url = f"http://{self.userpass}@127.0.0.1:{self.rpc_port}"
        self.rpc = AuthServiceProxy(self.rpc_url)

        # Wait for bitcoind to be ready
        ready = False
        while not ready:
            try:
                self.rpc.getblockchaininfo()
                ready = True
            except JSONRPCException:
                time.sleep(0.5)
                pass

        assert self.rpc.getblockchaininfo()['chain'] == 'regtest'
        self.version = self.rpc.getnetworkinfo()['version']
        assert self.version >= 220000, "we require >= 22.0 of Core"
        # not descriptors so that we can do dumpwallet
        try:
            self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=False)
        except JSONRPCException as e:
            assert "BDB wallet creation is deprecated" in str(e) \
                   or "no longer possible to create a legacy wallet" in str(e) # before v30.0 vs v30.0+
            self.has_bdb = False
            self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=True)

        # Make sure there are blocks and coins available
        self.supply_wallet.generatetoaddress(101, self.supply_wallet.getnewaddress())

    def get_wallet_rpc(self, wallet):
        url = self.rpc_url + f"/wallet/{wallet}"
        return AuthServiceProxy(url)

    def create_wallet(self, wallet_name: str, disable_private_keys: bool = False, blank: bool = False,
                      passphrase: str = None, avoid_reuse: bool = False, descriptors: bool = True,
                      load_on_startup: bool = False, external_signer: bool = False) -> AuthServiceProxy:
        """Create wallet and return AuthServiceProxy object to that wallet"""
        self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=disable_private_keys,
                              blank=blank, passphrase=passphrase, avoid_reuse=avoid_reuse,
                              descriptors=descriptors, load_on_startup=load_on_startup,
                              external_signer=external_signer)
        return self.get_wallet_rpc(wallet_name)

    def cleanup(self, *args, **kwargs):
        if self.bitcoind_proc is not None and self.bitcoind_proc.poll() is None:
            self.bitcoind_proc.kill()
        time.sleep(0.5)
        shutil.rmtree(self.datadir)

    def delete_wallet_files(self, pattern=None):
        wallets_dir = os.path.join(self.datadir, "regtest/wallets")
        wallet_files = os.listdir(wallets_dir)
        for wf in wallet_files:
            abs_path = os.path.join(wallets_dir, wf)
            if pattern is None:
                # remove all
                shutil.rmtree(abs_path)
            else:
                if pattern in wf:
                    shutil.rmtree(abs_path)

    @staticmethod
    def create(*args, **kwargs):
        c = Bitcoind(*args, **kwargs)
        c.start()
        return c


@pytest.fixture
def bitcoind():
    # JSON-RPC connection to a bitcoind instance
    # this assumes that you have bitcoind in path somewhere
    bitcoin_d = Bitcoind.create()
    yield bitcoin_d
    os.killpg(os.getpgid(bitcoin_d.bitcoind_proc.pid), signal.SIGTERM)


@pytest.fixture
def match_key(bitcoind, set_master_key, reset_seed_words):
    # load simulator w/ existing bip32 master key of testnet instance

    # bummer: dumpmasterprivkey RPC call was removed!
    #prv = bitcoind.dumpmasterprivkey()

    # bummer: dumpwallet RPC call was removed does not work with descriptor wallets
    try:
        from tempfile import mktemp
        fn = mktemp()
        bitcoind.supply_wallet.dumpwallet(fn)
        prv = None

        for ln in open(fn, 'rt').readlines():
            if 'extended private masterkey' in ln:
                assert not prv
                prv = ln.split(": ", 1)[1].strip()

        os.unlink(fn)
    except JSONRPCException as e:
        assert "Only legacy wallets are supported by this command" in str(e) \
               or "Method not found" in str(e)  # v30.0

        prv_descs = bitcoind.supply_wallet.listdescriptors(True)  # True --> show private
        prv = prv_descs["descriptors"][0]["desc"].replace("pkh(", "").split("/")[0]

    assert prv.startswith('tprv')

    xfp = set_master_key(prv)

    yield xfp


@pytest.fixture
def finalize_v2_v0_convert(bitcoind):
    def doit(psbt_obj):
        # compat wrapper - can be removed after below released
        # https://github.com/bitcoin/bitcoin/pull/21283 PSBTv2
        # convert v2 -> v0 if bitcoind does not support PSBTv2
        # to be able to finalize
        from authproxy import JSONRPCException
        try:
            resp = bitcoind.supply_wallet.finalizepsbt(psbt_obj.as_b64_str())
        except JSONRPCException as e:
            assert "Unsupported version number" in e.error["message"]
            # this version of bitcoind does not support PSBTv2
            # convert to v0 - needed for finalize
            resp = bitcoind.supply_wallet.finalizepsbt(
                base64.b64encode(psbt_obj.to_v0()).decode()
            )
        return resp

    return doit

@pytest.fixture
def bitcoind_wallet(bitcoind):
    # Use bitcoind to create a temporary wallet file
    w_name = 'ckcc-test-wallet-%s' % uuid.uuid4()
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True,
                                  passphrase=None, avoid_reuse=False, descriptors=not bitcoind.has_bdb)
    yield conn


@pytest.fixture
def bitcoind_d_wallet(bitcoind):
    # Use bitcoind to create a temporary DESCRIPTOR-based wallet file
    w_name = 'ckcc-test-desc-wallet-%s' % uuid.uuid4()
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True,
                                  passphrase=None, avoid_reuse=False, descriptors=True)
    yield conn


@pytest.fixture
def bitcoind_d_wallet_w_sk(bitcoind):
    # Use bitcoind to create a temporary DESCRIPTOR-based wallet file
    w_name = 'ckcc-test-desc-wallet-w-sk-%s' % uuid.uuid4()
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=False, blank=False,
                                  passphrase=None, avoid_reuse=False, descriptors=True)
    yield conn


@pytest.fixture
def bitcoind_d_sim_watch(bitcoind):
    # watch only descriptor wallet simulator
    w_name = 'ckcc-test-desc-wallet-sim-%s' % uuid.uuid4()
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True,
                                  passphrase=None, avoid_reuse=False, descriptors=True)
    descriptors = [
        {
            "timestamp": "now",
            "active": True,
            "desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/0/*)#erexmnep",
            "internal": False
        },
        {
            "desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/1/*)#ghu8xxfe",
            "active": True,
            "internal": True,
            "timestamp": "now"
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/0/*)#6ghw47ge",
            "internal": False
        },
        {
            "desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/1/*)#tuj0gtcp",
            "active": True,
            "internal": True,
            "timestamp": "now"
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/0/*)#fxwk08tc",
            "internal": False
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/1/*)#cjthjjmq",
            "internal": True
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/0/*))#weah3vek",
            "internal": False
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/1/*))#mcnpfnvf",
            "internal": True
        },
    ]
    conn.importdescriptors(descriptors)
    yield conn


@pytest.fixture
def bitcoind_d_dev_watch(request, dev, bitcoind, dev_core_import_object):
    name = ""

    if dev.is_simulator:
        settings_set = request.getfixturevalue('settings_set')
        settings_set("chain", "XRT")
        name += "sim"

    assert dev.send_recv(CCProtocolPacker.block_chain()) == "XRT", "needs regtest"
    xfp = xfp2str(dev.master_fingerprint)
    name += xfp
    name += "watch-only"
    w_name = '%s-%s' % (name, uuid.uuid4())
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True,
                                  passphrase=None, avoid_reuse=False, descriptors=True)

    conn.importdescriptors(dev_core_import_object)
    yield conn

@pytest.fixture
def bitcoind_d_sim_sign(bitcoind):
    # Use bitcoind to create a clone of simulator wallet with private keys
    w_name = 'ckcc-test-desc-wallet-sim-%s' % uuid.uuid4()
    conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=False, blank=True,
                                  passphrase=None, avoid_reuse=False, descriptors=True)
    # below is simulator descriptor wallet
    descriptors = [
        {
            "timestamp": "now",
            "active": True,
            "desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/0/*)#mzg0pna0",
            "internal": False
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/1/*)#2kdwuxdh",
            "internal": True
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/0/*)#x7dfk9mw",
            "internal": False
        },
        {
            "desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/1/*)#h2ggtstk",
            "active": True,
            "internal": True,
            "timestamp": "now"
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/0/*)#kjnlnm3v",
            "internal": False
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/1/*)#8xk7wwp5",
            "internal": True
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/0/*))#0qf5gv2y",
            "internal": False
        },
        {
            "timestamp": "now",
            "active": True,
            "desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/1/*))#6p8zsnlm",
            "internal": True
        },
    ]
    conn.importdescriptors(descriptors)
    yield conn

# EOF
