// Copyright (c) 2016-2024 The Hush developers
// Distributed under the GPLv3 software license, see the accompanying
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
//
// Tests for HD-derived transparent keys (m/44'/coin'/0'/0/i) and the
// version-gated CHDChain serialization used to persist the transparent counter.
#include <gtest/gtest.h>

#include "key.h"
#include "chainparams.h"
#include "streams.h"
#include "uint256.h"
#include "util.h"
#include "version.h"
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#include "zcash/zip32.h"

// Build an in-memory wallet with a known seed + hdChain so that the
// HD-transparent path (IsHDTransparentEnabled) is active.
static void LoadSeedForTest(CWallet& wallet, const HDSeed& seed)
{
    wallet.LoadHDSeed(seed);
    CHDChain chain;
    chain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
    chain.seedFp = seed.Fingerprint();
    chain.nCreateTime = 1;
    wallet.SetHDChain(chain, true /* memonly */);
}

// Same seed must reproduce the same transparent addresses in the same order:
// this is the recovery guarantee that lets a seed-only restore find coinbase.
TEST(hdtransparent_tests, DeterministicFromSeed)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
    HDSeed seed(rawSeed);

    std::vector<CKeyID> keysA;
    {
        CWallet wallet;
        LoadSeedForTest(wallet, seed);
        ASSERT_TRUE(wallet.IsHDTransparentEnabled());

        LOCK(wallet.cs_wallet);
        for (int i = 0; i < 5; i++) {
            CPubKey pk = wallet.GenerateNewKey();
            keysA.push_back(pk.GetID());

            const CKeyMetadata& md = wallet.mapKeyMetadata[pk.GetID()];
            EXPECT_EQ(md.seedFp, seed.Fingerprint());
            EXPECT_EQ(md.hdKeypath, std::string("m/44'/141'/0'/0/") + std::to_string(i));
        }
    }

    // Fresh wallet, same seed -> identical keys.
    {
        CWallet wallet;
        LoadSeedForTest(wallet, seed);

        LOCK(wallet.cs_wallet);
        for (int i = 0; i < 5; i++) {
            CPubKey pk = wallet.GenerateNewKey();
            EXPECT_EQ(pk.GetID(), keysA[i]);
        }
    }
}

// Pin the exact derivation path so it can never silently change.
TEST(hdtransparent_tests, KnownDerivationPath)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
    HDSeed seed(rawSeed);

    // Independently derive m/44'/141'/0'/0/0.
    RawHDSeed raw = seed.RawSeed();
    CExtKey m, purpose, coinType, account, external, child;
    m.SetMaster(raw.data(), raw.size());
    m.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT);
    purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT);
    coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT);
    account.Derive(external, 0);
    external.Derive(child, 0);
    CKeyID expected = child.key.GetPubKey().GetID();

    CWallet wallet;
    LoadSeedForTest(wallet, seed);
    LOCK(wallet.cs_wallet);
    CPubKey pk = wallet.GenerateNewKey();
    EXPECT_EQ(pk.GetID(), expected);
}

// A pre-existing v1 CHDChain record (no transparent counter) must still
// deserialize under v2 code, leaving transparentChildCounter at 0; and a v2
// record must round-trip the counter.
TEST(hdtransparent_tests, HDChainVersionCompat)
{
    CHDChain v1;
    v1.nVersion = CHDChain::VERSION_HD_BASE; // 1: transparentChildCounter not serialized
    v1.seedFp = uint256S("0000000000000000000000000000000000000000000000000000000000000001");
    v1.nCreateTime = 12345;
    v1.saplingAccountCounter = 7;

    CDataStream ss(SER_DISK, PROTOCOL_VERSION);
    ss << v1;

    CHDChain out; // default-constructed: SetNull() zeroes transparentChildCounter
    ss >> out;
    EXPECT_EQ(out.nVersion, +CHDChain::VERSION_HD_BASE); // unary + -> rvalue, avoid ODR-use of static const
    EXPECT_EQ(out.seedFp, v1.seedFp);
    EXPECT_EQ(out.nCreateTime, (int64_t)12345);
    EXPECT_EQ(out.saplingAccountCounter, (uint32_t)7);
    EXPECT_EQ(out.transparentChildCounter, (uint32_t)0);

    CHDChain v2;
    v2.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
    v2.saplingAccountCounter = 3;
    v2.transparentChildCounter = 42;
    CDataStream ss2(SER_DISK, PROTOCOL_VERSION);
    ss2 << v2;

    CHDChain out2;
    ss2 >> out2;
    EXPECT_EQ(out2.nVersion, +CHDChain::VERSION_HD_TRANSPARENT);
    EXPECT_EQ(out2.saplingAccountCounter, (uint32_t)3);
    EXPECT_EQ(out2.transparentChildCounter, (uint32_t)42);
}

// Restoring from a 32-byte seed hex reproduces the same keys as the source
// wallet, and refuses to run when a seed already exists.
TEST(hdtransparent_tests, RestoreFromSeedHex)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x7a);
    HDSeed seed(rawSeed);
    std::string seedHex = HexStr(seed.RawSeed());

    // Source wallet: derive some keys.
    std::vector<CKeyID> expected;
    {
        CWallet wallet;
        LoadSeedForTest(wallet, seed);
        LOCK(wallet.cs_wallet);
        for (int i = 0; i < 3; i++)
            expected.push_back(wallet.GenerateNewKey().GetID());
    }

    // Restored wallet: inject the seed hex, pre-derive, and compare.
    {
        CWallet wallet;
        ASSERT_TRUE(wallet.SetHDSeedFromHex(seedHex));
        // Second attempt must fail: a seed already exists.
        EXPECT_FALSE(wallet.SetHDSeedFromHex(seedHex));

        wallet.TopUpHDTransparentKeys(3, 1);
        LOCK(wallet.cs_wallet);
        for (int i = 0; i < 3; i++)
            EXPECT_TRUE(wallet.HaveKey(expected[i]));
    }

    // Bad input is rejected.
    {
        CWallet wallet;
        EXPECT_FALSE(wallet.SetHDSeedFromHex("nothex"));
        EXPECT_FALSE(wallet.SetHDSeedFromHex("abcd")); // too short
    }
}
