// Copyright (c) 2020-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <chainparams.h>
#include <consensus/amount.h>
#include <consensus/merkle.h>
#include <kernel/coinstats.h>
#include <node/miner.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <sync.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/util/mining.h>
#include <test/util/random.h>
#include <test/util/setup_common.h>
#include <test/util/time.h>
#include <txdb.h>
#include <uint256.h>
#include <util/check.h>
#include <validation.h>

#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

FUZZ_TARGET(utxo_total_supply)
{
    SeedRandomStateForTest(SeedRand::ZEROS);
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
    NodeClockContext clock_ctx{ConsumeTime(fuzzed_data_provider, /*min=*/1296688602)}; // regtest genesis block timestamp
    /** The testing setup that creates a chainman only (no chainstate) */
    ChainTestingSetup test_setup{
        ChainType::REGTEST,
        {
            .extra_args = {
                "-testactivationheight=bip34@2",
            },
        },
    };
    // Create chainstate
    test_setup.LoadVerifyActivateChainstate();
    auto& node{test_setup.m_node};
    auto& chainman{*Assert(test_setup.m_node.chainman)};

    const auto ActiveHeight = [&]() {
        LOCK(chainman.GetMutex());
        return chainman.ActiveHeight();
    };
    const auto PrepareNextBlock = [&]() {
        // Use OP_FALSE to avoid BIP30 check from hitting early
        auto block = PrepareBlock(node, {
            .coinbase_output_script = CScript() << OP_FALSE,
        });
        // Replace OP_FALSE with OP_TRUE
        {
            CMutableTransaction tx{*block->vtx.back()};
            tx.nLockTime = 0; // Use the same nLockTime for all as we want to duplicate one of them.
            tx.vout.at(0).scriptPubKey = CScript{} << OP_TRUE;
            block->vtx.back() = MakeTransactionRef(tx);
        }
        return block;
    };

    /** The block template this fuzzer is working on */
    auto current_block = PrepareNextBlock();
    /** Append-only set of tx outpoints, entries are not removed when spent */
    std::vector<std::pair<COutPoint, CTxOut>> txos;
    /** The utxo stats at the chain tip */
    kernel::CCoinsStats utxo_stats;
    /** The total amount of coins in the utxo set */
    CAmount circulation{0};


    // Store the tx out in the txo map
    const auto StoreLastTxo = [&]() {
        // get last tx
        const CTransaction& tx = *current_block->vtx.back();
        // get last out
        const uint32_t i = tx.vout.size() - 1;
        // store it
        txos.emplace_back(COutPoint{tx.GetHash(), i}, tx.vout.at(i));
        if (current_block->vtx.size() == 1 && tx.vout.at(i).scriptPubKey[0] == OP_RETURN) {
            // also store coinbase
            const uint32_t i = tx.vout.size() - 2;
            txos.emplace_back(COutPoint{tx.GetHash(), i}, tx.vout.at(i));
        }
    };
    const auto AppendRandomTxo = [&](CMutableTransaction& tx) {
        const auto& txo = txos.at(fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, txos.size() - 1));
        tx.vin.emplace_back(txo.first);
        tx.vout.emplace_back(txo.second.nValue, txo.second.scriptPubKey); // "Forward" coin with no fee
    };
    const auto UpdateUtxoStats = [&](bool wipe_cache) {
        LOCK(chainman.GetMutex());
        chainman.ActiveChainstate().ForceFlushStateToDisk(wipe_cache);
        utxo_stats = std::move(
            *Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::NONE, &chainman.ActiveChainstate().CoinsDB(), chainman.m_blockman, {})));
        // Check that miner can't print more money than they are allowed to
        assert(circulation == utxo_stats.total_amount);
    };


    // Update internal state to chain tip
    StoreLastTxo();
    UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
    assert(ActiveHeight() == 0);
    // Get at which height we duplicate the coinbase
    // Assuming that the fuzzer will mine relatively short chains (less than 200 blocks), we want the duplicate coinbase to be not too high.
    // Up to 300 seems reasonable.
    int64_t duplicate_coinbase_height = fuzzed_data_provider.ConsumeIntegralInRange(0, 300);
    // Avoid bad-cb-length error at heights <= 16. Pad the BIP34-encoded height
    // with OP_0 to satisfy the minimum 2-byte coinbase scriptSig length.
    CScript duplicate_coinbase_script = CScript() << duplicate_coinbase_height;
    if (duplicate_coinbase_height <= 16) {
        duplicate_coinbase_script << OP_0;
    }
    // Mine the first block with this duplicate
    current_block = PrepareNextBlock();
    StoreLastTxo();

    {
        // Create duplicate (CScript should match exact format as in CreateNewBlock)
        CMutableTransaction tx{*current_block->vtx.front()};
        tx.vin.at(0).scriptSig = duplicate_coinbase_script;

        // Mine block and create next block template
        current_block->vtx.front() = MakeTransactionRef(tx);
    }
    current_block->hashMerkleRoot = BlockMerkleRoot(*current_block);
    assert(!MineBlock(node, current_block).IsNull());
    circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());

    assert(ActiveHeight() == 1);
    UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
    current_block = PrepareNextBlock();
    StoreLastTxo();

    // Limit to avoid timeout, but enough to cover duplicate_coinbase_height
    // and CVE-2018-17144.
    LIMITED_WHILE(fuzzed_data_provider.remaining_bytes(), 2'00)
    {
        CallOneOf(
            fuzzed_data_provider,
            [&] {
                // Append an input-output pair to the last tx in the current block
                CMutableTransaction tx{*current_block->vtx.back()};
                AppendRandomTxo(tx);
                current_block->vtx.back() = MakeTransactionRef(tx);
                StoreLastTxo();
            },
            [&] {
                // Append a tx to the list of txs in the current block
                CMutableTransaction tx{};
                AppendRandomTxo(tx);
                current_block->vtx.push_back(MakeTransactionRef(tx));
                StoreLastTxo();
            },
            [&] {
                // Append the current block to the active chain
                node::RegenerateCommitments(*current_block, chainman);
                const bool was_valid = !MineBlock(node, current_block).IsNull();

                const uint256 prev_hash_serialized{utxo_stats.hashSerialized};
                if (was_valid) {
                    if (duplicate_coinbase_height == ActiveHeight()) {
                        // we mined the duplicate coinbase
                        assert(current_block->vtx.at(0)->vin.at(0).scriptSig == duplicate_coinbase_script);
                    }

                    circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());
                }

                UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());

                if (!was_valid) {
                    // utxo stats must not change
                    assert(prev_hash_serialized == utxo_stats.hashSerialized);
                }

                current_block = PrepareNextBlock();
                StoreLastTxo();
            });
    }
}
