#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.

import os
import random
import logging
import unittest

import basicswap.config as cfg
from basicswap.basicswap import (
    Coins,
)
from basicswap.util.address import (
    toWIF,
)
from tests.basicswap.common import (
    stopDaemons,
    make_rpc_func,
    waitForRPC,
)

from basicswap.bin.run import startDaemon
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from tests.basicswap.test_xmr import test_delay_event, callnoderpc
from basicswap.contrib.test_framework.messages import (
    CTransaction,
    CTxIn,
    COutPoint,
)
from basicswap.contrib.test_framework.script import (
    CScript,
    OP_CHECKLOCKTIMEVERIFY,
)


from tests.basicswap.test_btc_xmr import TestFunctions

logger = logging.getLogger()

DOGE_BINDIR = os.path.expanduser(
    os.getenv("DOGE_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "dogecoin"))
)
DOGED = os.getenv("DOGED", "dogecoind" + cfg.bin_suffix)
DOGE_CLI = os.getenv("DOGE_CLI", "dogecoin-cli" + cfg.bin_suffix)
DOGE_TX = os.getenv("DOGE_TX", "dogecoin-tx" + cfg.bin_suffix)


DOGE_BASE_PORT = 22556
DOGE_BASE_RPC_PORT = 18442


def prepareDataDir(
    datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3
):
    node_dir = os.path.join(datadir, dir_prefix + str(node_id))
    if not os.path.exists(node_dir):
        os.makedirs(node_dir)
    cfg_file_path = os.path.join(node_dir, conf_file)
    if os.path.exists(cfg_file_path):
        return
    with open(cfg_file_path, "w+") as fp:
        fp.write("regtest=1\n")
        fp.write("[regtest]\n")
        fp.write("port=" + str(base_p2p_port + node_id) + "\n")
        fp.write("rpcport=" + str(base_rpc_port + node_id) + "\n")

        salt = generate_salt(16)
        fp.write(
            "rpcauth={}:{}${}\n".format(
                "test" + str(node_id),
                salt,
                password_to_hmac(salt, "test_pass" + str(node_id)),
            )
        )

        fp.write("daemon=0\n")
        fp.write("printtoconsole=0\n")
        fp.write("server=1\n")
        fp.write("discover=0\n")
        fp.write("listenonion=0\n")
        fp.write("bind=127.0.0.1\n")
        fp.write("findpeers=0\n")
        fp.write("debug=1\n")
        fp.write("debugexclude=libevent\n")

        fp.write("acceptnonstdtxn=0\n")

        for i in range(0, num_nodes):
            if node_id == i:
                continue
            fp.write("addnode=127.0.0.1:{}\n".format(base_p2p_port + i))

    return node_dir


class Test(TestFunctions):
    __test__ = True
    test_coin = Coins.DOGE
    test_coin_from = Coins.BTC
    test_coin_to = Coins.DOGE
    doge_daemons = []
    doge_addr = None
    start_ltc_nodes = False
    start_xmr_nodes = False

    test_atomic = False
    test_xmr = True

    pause_chain = False

    doge_seeds = [
        "516b471da2a67bcfd42a1da7f7ae8f9a1b02c34f6a2d6a943ceec5dca68e7fa1",
        "a8c0911fba070d5cc2784703afeb0f7c3b9b524b8a53466c04e01933d9fede78",
        "7b3b533ac3a27114ae17c8cca0d2cd9f736e7519ae52b8ec8f1f452e8223d082",
    ]

    @classmethod
    def prepareExtraDataDir(cls, i):
        if not cls.restore_instance:
            prepareDataDir(
                cfg.TEST_DATADIRS,
                i,
                "dogecoin.conf",
                "doge_",
                base_p2p_port=DOGE_BASE_PORT,
                base_rpc_port=DOGE_BASE_RPC_PORT,
            )
        cls.doge_daemons.append(
            startDaemon(
                os.path.join(cfg.TEST_DATADIRS, "doge_" + str(i)),
                DOGE_BINDIR,
                DOGED,
            )
        )
        logging.info("Started %s %d", DOGED, cls.doge_daemons[-1].handle.pid)

        dogeRpc = make_rpc_func(i, base_rpc_port=DOGE_BASE_RPC_PORT)
        waitForRPC(dogeRpc, test_delay_event, rpc_command="getblockchaininfo")
        if len(dogeRpc("listwallets")) < 1:
            dogeRpc("createwallet", ["bsx_wallet", False, True, "", False, False])
            wif_prefix: int = 239
            wif = toWIF(wif_prefix, bytes.fromhex(cls.doge_seeds[i]), False)
            dogeRpc("sethdseed", [True, wif])

        waitForRPC(
            dogeRpc,
            test_delay_event,
            max_tries=12,
        )

    @classmethod
    def addPIDInfo(cls, sc, i):
        sc.setDaemonPID(Coins.DOGE, cls.doge_daemons[i].handle.pid)

    @classmethod
    def sync_blocks(cls, wait_for: int = 20, num_nodes: int = 3) -> None:
        logging.info("Syncing blocks")
        for i in range(wait_for):
            if test_delay_event.is_set():
                raise ValueError("Test stopped.")
            block_hash0 = callnoderpc(
                0, "getbestblockhash", base_rpc_port=DOGE_BASE_RPC_PORT
            )
            matches: int = 0
            for i in range(1, num_nodes):
                block_hash = callnoderpc(
                    i, "getbestblockhash", base_rpc_port=DOGE_BASE_RPC_PORT
                )
                if block_hash == block_hash0:
                    matches += 1
            if matches == num_nodes - 1:
                return
            test_delay_event.wait(1)
        raise ValueError("sync_blocks timed out.")

    @classmethod
    def prepareExtraCoins(cls):
        super().prepareExtraCoins()
        if cls.restore_instance:
            void_block_rewards_pubkey = cls.getRandomPubkey()
            cls.doge_addr = (
                cls.swap_clients[0]
                .ci(Coins.DOGE)
                .pubkey_to_address(void_block_rewards_pubkey)
            )
        else:
            num_blocks = 400
            cls.doge_addr = callnoderpc(
                0, "getnewaddress", ["mining_addr"], base_rpc_port=DOGE_BASE_RPC_PORT
            )

            logging.info("Mining %d DOGE blocks to %s", num_blocks, cls.doge_addr)
            callnoderpc(
                0,
                "generatetoaddress",
                [num_blocks, cls.doge_addr],
                base_rpc_port=DOGE_BASE_RPC_PORT,
            )

            doge_addr1 = callnoderpc(
                1, "getnewaddress", ["initial addr"], base_rpc_port=DOGE_BASE_RPC_PORT
            )
            for i in range(5):
                callnoderpc(
                    0,
                    "sendtoaddress",
                    [doge_addr1, 1000],
                    base_rpc_port=DOGE_BASE_RPC_PORT,
                )

            # Set future block rewards to nowhere (a random address), so wallet amounts stay constant
            void_block_rewards_pubkey = cls.getRandomPubkey()
            cls.doge_addr = (
                cls.swap_clients[0]
                .ci(Coins.DOGE)
                .pubkey_to_address(void_block_rewards_pubkey)
            )
            num_blocks = 100
            logging.info("Mining %d DOGE blocks to %s", num_blocks, cls.doge_addr)
            callnoderpc(
                0,
                "generatetoaddress",
                [num_blocks, cls.doge_addr],
                base_rpc_port=DOGE_BASE_RPC_PORT,
            )

        cls.sync_blocks()

    @classmethod
    def tearDownClass(cls):
        logging.info("Finalising DOGE Test")
        super().tearDownClass()

        stopDaemons(cls.doge_daemons)
        cls.doge_daemons.clear()

    @classmethod
    def addCoinSettings(cls, settings, datadir, node_id):
        settings["chainclients"]["dogecoin"] = {
            "connection_type": "rpc",
            "manage_daemon": False,
            "rpcport": DOGE_BASE_RPC_PORT + node_id,
            "rpcuser": "test" + str(node_id),
            "rpcpassword": "test_pass" + str(node_id),
            "datadir": os.path.join(datadir, "doge_" + str(node_id)),
            "bindir": DOGE_BINDIR,
            "use_csv": False,
            "use_segwit": False,
            "blocks_confirmed": 1,
            "min_relay_fee": 0.01,  # RECOMMENDED_MIN_TX_FEE
            "wallet_name": "bsx_wallet",
        }

    @classmethod
    def coins_loop(cls):
        super().coins_loop()
        if cls.pause_chain:
            return
        ci0 = cls.swap_clients[0].ci(cls.test_coin)
        try:
            if cls.doge_addr is not None:
                ci0.rpc_wallet("generatetoaddress", [1, cls.doge_addr])
        except Exception as e:
            logging.warning("coins_loop generate {}".format(e))

    def callnoderpc(self, method, params=[], wallet=None, node_id=0):
        return callnoderpc(
            node_id, method, params, wallet, base_rpc_port=DOGE_BASE_RPC_PORT
        )

    def mineBlock(self, num_blocks: int = 1):
        self.callnoderpc("generatetoaddress", [num_blocks, self.doge_addr])

    def test_003_cltv(self):
        logging.info("---------- Test {} cltv".format(self.test_coin.name))
        ci = self.swap_clients[0].ci(self.test_coin)

        self.pause_chain = True
        try:
            start_height: int = self.callnoderpc("getblockcount")

            num_blocks: int = 1351  # consensus.BIP65Height = 1351;

            if start_height < num_blocks:
                to_mine = num_blocks - start_height
                logging.info("Mining %d DOGE blocks to %s", to_mine, self.doge_addr)
                ci.rpc("generatetoaddress", [to_mine, self.doge_addr])

            # self.check_softfork_active("bip65")  # TODO: Re-enable next version

            chain_height: int = self.callnoderpc("getblockcount")

            script = CScript(
                [
                    chain_height + 3,
                    OP_CHECKLOCKTIMEVERIFY,
                ]
            )
            script_dest = ci.getScriptDest(script)
            script_info = ci.rpc_wallet(
                "decodescript",
                [
                    script_dest.hex(),
                ],
            )
            script_addr = ci.encodeScriptDest(script_dest)
            assert script_info["address"] == script_addr

            prevout_amount: int = ci.make_int(1.1)
            tx = CTransaction()
            tx.nVersion = ci.txVersion()
            tx.vout.append(ci.txoType()(prevout_amount, script_dest))
            tx_hex = tx.serialize().hex()

            tx = CTransaction()
            tx.nVersion = ci.txVersion()
            tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
            tx_hex = tx.serialize().hex()
            tx_funded = ci.rpc_wallet("fundrawtransaction", [tx_hex])
            utxo_pos = 0 if tx_funded["changepos"] == 1 else 1
            tx_signed = ci.rpc_wallet(
                "signrawtransactionwithwallet",
                [
                    tx_funded["hex"],
                ],
            )["hex"]
            txid = ci.rpc(
                "sendrawtransaction",
                [
                    tx_signed,
                ],
            )

            addr_out = ci.rpc_wallet(
                "getnewaddress",
                [
                    "cltv test",
                ],
            )
            pkh = ci.decodeAddress(addr_out)
            script_out = ci.getScriptForPubkeyHash(pkh)

            tx_spend = CTransaction()
            tx_spend.nVersion = ci.txVersion()
            tx_spend.nLockTime = chain_height + 3
            tx_spend.vin.append(
                CTxIn(
                    COutPoint(int(txid, 16), utxo_pos),
                    scriptSig=CScript(
                        [
                            script,
                        ]
                    ),
                )
            )
            tx_spend.vout.append(ci.txoType()(ci.make_int(1.099), script_out))
            tx_spend_hex = tx_spend.serialize().hex()

            tx_spend.nLockTime = chain_height + 2
            tx_spend_invalid_hex = tx_spend.serialize().hex()

            for tx_hex in [tx_spend_invalid_hex, tx_spend_hex]:
                try:
                    txid = self.callnoderpc(
                        "sendrawtransaction",
                        [
                            tx_hex,
                        ],
                    )
                except Exception as e:
                    assert "non-final" in str(
                        e
                    ) or "Locktime requirement not satisfied" in str(e)
                else:
                    assert False, "Should fail"

            self.mineBlock(5)

            txid = ci.rpc(
                "sendrawtransaction",
                [
                    tx_spend_hex,
                ],
            )
            self.mineBlock()
            ci.rpc("syncwithvalidationinterfacequeue")
            # Ensure tx was mined
            tx_wallet = ci.rpc_wallet(
                "gettransaction",
                [
                    txid,
                ],
            )
            assert len(tx_wallet["blockhash"]) == 64
        finally:
            self.pause_chain = False

    def test_010_txn_size(self):
        logging.info("---------- Test {} txn size".format(self.test_coin.name))

        swap_clients = self.swap_clients
        ci = swap_clients[0].ci(self.test_coin)

        amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)

        # fee_rate is in sats/kvB
        fee_rate: int = 1000000

        # Test chain b (no-script) lock tx size
        v = ci.getNewRandomKey()
        s = ci.getNewRandomKey()
        S = ci.getPubkey(s)
        result = ci.publishBLockTx(v, S, amount, fee_rate)
        if isinstance(result, tuple):
            lock_tx_b_txid, lock_tx_b_vout = result
        else:
            lock_tx_b_txid = result
        test_delay_event.wait(1)

        addr_out = ci.getNewAddress(False)
        lock_tx_b_spend_txid = ci.spendBLockTx(
            lock_tx_b_txid, addr_out, v, s, amount, fee_rate, 0
        )
        test_delay_event.wait(1)

        lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid)
        if lock_tx_b_spend is None:
            lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid)
        assert lock_tx_b_spend is not None

        tx_obj = ci.loadTx(lock_tx_b_spend)
        tx_out_value: int = tx_obj.vout[0].nValue
        fee_paid = amount - tx_out_value

        actual_size = len(lock_tx_b_spend)
        expect_size: int = ci.xmr_swap_b_lock_spend_tx_vsize()
        fee_expect = round(fee_rate * expect_size / 1000)
        assert fee_expect == fee_paid
        assert expect_size >= actual_size
        assert expect_size - actual_size < 10

    def test_01_a_full_swap(self):
        self.do_test_01_full_swap(self.test_coin_from, self.test_coin_to)

    def test_01_b_full_swap_reverse(self):
        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1801)
        self.do_test_01_full_swap(self.test_coin_to, self.test_coin_from)

    def test_01_c_full_swap_to_part(self):
        self.do_test_01_full_swap(self.test_coin, Coins.PART)

    def test_01_d_full_swap_from_part(self):
        self.do_test_01_full_swap(Coins.PART, self.test_coin)

    def test_02_a_leader_recover_a_lock_tx(self):
        self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, self.test_coin_to)

    def test_02_b_leader_recover_a_lock_tx_reverse(self):
        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1801)
        self.do_test_02_leader_recover_a_lock_tx(self.test_coin_to, self.test_coin_from)

    def test_03_a_follower_recover_a_lock_tx(self):
        self.do_test_03_follower_recover_a_lock_tx(
            self.test_coin_from, self.test_coin_to
        )

    def test_03_b_follower_recover_a_lock_tx_reverse(self):
        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1801)
        self.do_test_03_follower_recover_a_lock_tx(
            self.test_coin_to, self.test_coin_from
        )

    def test_03_e_follower_recover_a_lock_tx_mercy_release(self):
        self.do_test_03_follower_recover_a_lock_tx(
            self.test_coin_from, self.test_coin_to, with_mercy=True
        )

    def test_03_f_follower_recover_a_lock_tx_mercy_release_reverse(self):
        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1801)
        self.prepare_balance(self.test_coin_from, 100.0, 1801, 1800)
        self.do_test_03_follower_recover_a_lock_tx(
            self.test_coin_to, self.test_coin_from, with_mercy=True
        )

    def test_04_a_follower_recover_b_lock_tx(self):
        self.do_test_04_follower_recover_b_lock_tx(
            self.test_coin_from, self.test_coin_to
        )

    def test_04_b_follower_recover_b_lock_tx_reverse(self):
        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1801)
        self.do_test_04_follower_recover_b_lock_tx(
            self.test_coin_to, self.test_coin_from
        )

    def test_05_self_bid(self):
        self.do_test_05_self_bid(self.test_coin_from, self.test_coin_to)


if __name__ == "__main__":
    unittest.main()
