#!/usr/bin/env python3
# Copyright (c) 2024- The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Test SyncCoinsTipAfterChainSync logic
"""


from test_framework.blocktools import create_block, create_coinbase
from test_framework.messages import (
    MSG_BLOCK,
    MSG_TYPE_MASK,
)
from test_framework.p2p import (
    CBlockHeader,
    msg_block,
    msg_headers,
    P2PDataStore,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
    assert_equal,
)


class P2PBlockDelay(P2PDataStore):
    def __init__(self, delay_block):
        self.delay_block = delay_block
        super().__init__()

    def on_getdata(self, message):
        for inv in message.inv:
            self.getdata_requests.append(inv.hash)
            if (inv.type & MSG_TYPE_MASK) == MSG_BLOCK:
                if inv.hash != self.delay_block:
                    self.send_message(msg_block(self.block_store[inv.hash]))

    def on_getheaders(self, message):
        pass

    def send_delayed(self):
        self.send_message(msg_block(self.block_store[self.delay_block]))


SYNC_CHECK_INTERVAL = 30


class SyncCoinsTipAfterChainSyncTest(BitcoinTestFramework):
    def set_test_params(self):
        self.setup_clean_chain = True
        self.num_nodes = 1
        # Set maxtipage to 1 to get us out of IBD after 1 block past our mocktime
        self.extra_args = [["-maxtipage=1"]]

    def run_test(self):
        NUM_BLOCKS = 3
        node = self.nodes[0]
        tip = int(node.getbestblockhash(), 16)
        blocks = []
        height = 1
        block_time = node.getblock(node.getbestblockhash())["time"] + 1
        # Set mock time to 2 past block time, so second block will exit IBD
        node.setmocktime(block_time + 2)

        # Prepare blocks without sending them to the node
        block_dict = {}
        for _ in range(NUM_BLOCKS):
            blocks.append(create_block(tip, create_coinbase(height), block_time))
            blocks[-1].solve()
            tip = blocks[-1].sha256
            block_time += 1
            height += 1
            block_dict[blocks[-1].sha256] = blocks[-1]
        delay_block = blocks[-1].sha256

        # Create peer which will not automatically send last block
        peer = node.add_outbound_p2p_connection(
            P2PBlockDelay(delay_block),
            p2p_idx=1,
            connection_type="outbound-full-relay",
        )
        peer.block_store = block_dict

        self.log.info(
            "Send headers message for first block, verify it won't sync because node is still in IBD"
        )
        headers_message = msg_headers()
        headers_message.headers = [CBlockHeader(blocks[0])]
        peer.send_message(headers_message)
        peer.sync_with_ping()
        assert_equal(node.getblockchaininfo()["initialblockdownload"], True)
        with node.assert_debug_log(
            ["Node is still in IBD, rescheduling post-IBD chainstate disk sync..."]
        ):
            node.mockscheduler(SYNC_CHECK_INTERVAL)

        self.log.info(
            "Send headers message for second block, verify it won't sync because node height has changed"
        )
        headers_message.headers = [CBlockHeader(blocks[1])]
        peer.send_message(headers_message)
        peer.sync_with_ping()
        assert_equal(node.getblockchaininfo()["initialblockdownload"], False)
        with node.assert_debug_log(
            [
                "Chain height updated since last check, rescheduling post-IBD chainstate disk sync..."
            ]
        ):
            node.mockscheduler(SYNC_CHECK_INTERVAL)

        self.log.info(
            "Send headers message for last block, verify it won't sync because node is still downloading the block"
        )
        headers_message.headers = [CBlockHeader(blocks[2])]
        peer.send_message(headers_message)
        peer.sync_with_ping()
        with node.assert_debug_log(
            [
                "Still downloading blocks from peers, rescheduling post-IBD chainstate disk sync..."
            ]
        ):
            node.mockscheduler(SYNC_CHECK_INTERVAL)

        self.log.info(
            "Send last block, verify it won't sync because node height has changed"
        )
        peer.send_delayed()
        peer.sync_with_ping()
        with node.assert_debug_log(
            [
                "Chain height updated since last check, rescheduling post-IBD chainstate disk sync..."
            ]
        ):
            node.mockscheduler(SYNC_CHECK_INTERVAL)

        self.log.info("Verify node syncs chainstate to disk on next scheduler update")
        with node.assert_debug_log(
            ["Finished syncing to tip, syncing chainstate to disk"]
        ):
            node.mockscheduler(SYNC_CHECK_INTERVAL)


if __name__ == "__main__":
    SyncCoinsTipAfterChainSyncTest(__file__).main()
