// 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
//
// Proves that a BIP39 seed phrase produces the SAME transparent and shielded
// addresses on the DragonX full node as in SilentDragonXLite. The proof chain:
//   phrase -> entropy (round-trip)  ->  64-byte BIP39 seed (known-answer)
//         -> z/t addresses (wallet path == direct ZIP-32/BIP44 derivation).
// The 64-byte seed is anchored to the well-known BIP39 value for the all-zero
// "abandon...art" entropy with an EMPTY passphrase, which is exactly what
// SilentDragonXLite's tiny-bip39 0.6.2 feeds into the same coin_type=141 paths.
#include <gtest/gtest.h>

#include "chainparams.h"
#include "key.h"
#include "key_io.h"
#include "util.h"
#include "wallet/mnemonic.h"
#include "wallet/wallet.h"
#include "zcash/Address.hpp"
#include "zcash/zip32.h"

// The canonical 24-word phrase for 32 bytes of all-zero entropy.
static const char* ABANDON_ART =
    "abandon abandon abandon abandon abandon abandon abandon abandon "
    "abandon abandon abandon abandon abandon abandon abandon abandon "
    "abandon abandon abandon abandon abandon abandon abandon art";

// The standard BIP39 seed for that phrase with an EMPTY passphrase
// (PBKDF2-HMAC-SHA512, 2048 rounds, salt "mnemonic"). Matches tiny-bip39.
static const char* SEED64_HEX =
    "408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf70"
    "5489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840";

// First shielded address for a 64-byte seed: m/32'/141'/0' default address.
static std::string DeriveZAddrFromSeed64(RawHDSeed seed64)
{
    HDSeed s(seed64);
    auto m = libzcash::SaplingExtendedSpendingKey::Master(s);
    auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT)
                 .Derive(141 | ZIP32_HARDENED_KEY_LIMIT)
                 .Derive(0 | ZIP32_HARDENED_KEY_LIMIT);
    return EncodePaymentAddress(xsk.DefaultAddress());
}

// First transparent address for a BIP32 master over `seedBytes`:
// m/44'/141'/0'/0/0.
static std::string DeriveTAddrFromSeedBytes(RawHDSeed seedBytes)
{
    CExtKey master, purpose, coinType, account, external, child;
    master.SetMaster(seedBytes.data(), seedBytes.size());
    master.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);
    return EncodeDestination(child.key.GetPubKey().GetID());
}

// The 64-byte seed derived from the mnemonic must equal the known BIP39 value.
// This is the cross-wallet anchor: SilentDragonXLite feeds the identical seed.
TEST(mnemonic_compat, Bip39SeedKnownAnswer)
{
    RawHDSeed entropy(32, 0);
    RawHDSeed seed64;
    ASSERT_TRUE(Bip39SeedFromEntropy(entropy, seed64));
    ASSERT_EQ(seed64.size(), (size_t)64);
    EXPECT_EQ(HexStr(seed64.begin(), seed64.end()), std::string(SEED64_HEX));
}

TEST(mnemonic_compat, EntropyPhraseRoundTrip)
{
    RawHDSeed zeros(32, 0);
    std::string phrase;
    ASSERT_TRUE(EntropyToMnemonic(zeros, phrase));
    EXPECT_EQ(phrase, std::string(ABANDON_ART));

    EXPECT_TRUE(MnemonicIsValid(ABANDON_ART));

    RawHDSeed entropy;
    ASSERT_TRUE(MnemonicToEntropy(ABANDON_ART, entropy));
    EXPECT_EQ(entropy.size(), (size_t)32);
    EXPECT_EQ(HexStr(entropy.begin(), entropy.end()), std::string(64, '0'));

    // Bad checksum / unknown words are rejected.
    EXPECT_FALSE(MnemonicIsValid("abandon abandon abandon"));
    EXPECT_FALSE(MnemonicIsValid("clearly not valid bip39 words at all here"));
    RawHDSeed junk;
    EXPECT_FALSE(MnemonicToEntropy("clearly not valid bip39 words at all here", junk));
}

// The wallet's mnemonic derivation must reproduce the exact addresses obtained
// by driving ZIP-32 / BIP44 directly from the known 64-byte seed, and must be
// deterministic across wallets.
TEST(mnemonic_compat, WalletDerivesSdxliteAddresses)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed zeros(32, 0), seed64;
    ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
    const std::string expZ = DeriveZAddrFromSeed64(seed64);
    const std::string expT = DeriveTAddrFromSeedBytes(seed64);
    EXPECT_EQ(expZ.substr(0, 2), "zs"); // sapling HRP for mainnet

    CWallet wallet;
    ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
    ASSERT_TRUE(wallet.IsMnemonicSeed());
    {
        LOCK(wallet.cs_wallet);
        EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()), expZ);
        EXPECT_EQ(EncodeDestination(wallet.GenerateNewKey().GetID()), expT);
    }

    // Same phrase, fresh wallet -> identical first addresses.
    CWallet wallet2;
    ASSERT_TRUE(wallet2.SetHDSeedFromMnemonic(ABANDON_ART));
    {
        LOCK(wallet2.cs_wallet);
        EXPECT_EQ(EncodePaymentAddress(wallet2.GenerateNewSaplingZKey()), expZ);
        EXPECT_EQ(EncodeDestination(wallet2.GenerateNewKey().GetID()), expT);
    }

    // The phrase round-trips out of the wallet.
    std::string exported;
    ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
    EXPECT_EQ(exported, std::string(ABANDON_ART));
}

// Negative: feeding the 32-byte entropy DIRECTLY as the seed (the classic
// interop bug) must produce a different address than the 64-byte BIP39 seed.
TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed zeros(32, 0), seed64;
    ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));

    const std::string seedT = DeriveTAddrFromSeedBytes(seed64); // correct (64-byte)
    const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte)
    EXPECT_NE(seedT, entropyT);
}
// New storage form: the HD seed IS the expanded 64-byte BIP39 seed, the chain is
// NOT flagged mnemonic, and the phrase comes from the separate entropy record.
TEST(mnemonic_compat, ExpandedSeedIsStoredDirectly)
{
    SelectParams(CBaseChainParams::MAIN);

    CWallet wallet;
    ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));

    // Stored bytes == the 64-byte BIP39 seed, fed to derivation unchanged.
    HDSeed stored;
    ASSERT_TRUE(wallet.GetHDSeed(stored));
    auto raw = stored.RawSeed();
    EXPECT_EQ(raw.size(), (size_t)64);
    EXPECT_EQ(HexStr(raw.begin(), raw.end()), std::string(SEED64_HEX));

    // No CHDChain version bump / no mnemonic flag: an older binary reads this
    // wallet and derives the same tree.
    EXPECT_FALSE(wallet.GetHDChain().fMnemonicSeed);
    EXPECT_LT(wallet.GetHDChain().nVersion, CHDChain::VERSION_HD_MNEMONIC);

    HDSeed forDerivation;
    ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
    EXPECT_EQ(forDerivation.RawSeed(), raw);

    // The phrase is still exportable, and IsMnemonicSeed() (which gates
    // z_exportmnemonic) still says yes.
    EXPECT_TRUE(wallet.IsMnemonicSeed());
    EXPECT_TRUE(wallet.HaveMnemonicEntropy());
    std::string exported;
    ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
    EXPECT_EQ(exported, std::string(ABANDON_ART));
}

// Backwards compatibility: a wallet in the OLD form (stored HD seed == 32-byte
// entropy, fMnemonicSeed = true) must still derive and still export its phrase.
TEST(mnemonic_compat, LegacyEntropySeedStillWorks)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed zeros(32, 0), seed64;
    ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));

    CWallet wallet;
    {
        LOCK(wallet.cs_wallet);
        RawHDSeed entropy(32, 0);
        HDSeed legacy(entropy);
        ASSERT_TRUE(wallet.InstallHDSeed(legacy, true, 1));
    }
    EXPECT_TRUE(wallet.GetHDChain().fMnemonicSeed);
    EXPECT_FALSE(wallet.HaveMnemonicEntropy());
    EXPECT_TRUE(wallet.IsMnemonicSeed());

    // Still expanded on read -> same key tree as the new form.
    HDSeed forDerivation;
    ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
    EXPECT_EQ(forDerivation.RawSeed(), seed64);
    {
        LOCK(wallet.cs_wallet);
        EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()),
                  DeriveZAddrFromSeed64(seed64));
    }

    std::string exported;
    ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
    EXPECT_EQ(exported, std::string(ABANDON_ART));
}

// The entropy record refuses replacement, and a phrase that does not restore the
// installed seed is never printed.
TEST(mnemonic_compat, MnemonicEntropyGuards)
{
    SelectParams(CBaseChainParams::MAIN);

    RawHDSeed zeros(32, 0), ones(32, 1);

    // Refuse-to-replace.
    CWallet wallet;
    ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
    EXPECT_FALSE(wallet.SetMnemonicEntropy(ones));
    EXPECT_FALSE(wallet.SetMnemonicEntropy(RawHDSeed())); // empty is not "installed"

    // Mismatched entropy -> no phrase. Install a 64-byte seed that is NOT the
    // expansion of `zeros`, then attach `zeros` as entropy.
    RawHDSeed otherSeed64;
    ASSERT_TRUE(Bip39SeedFromEntropy(ones, otherSeed64));
    CWallet mismatched;
    ASSERT_TRUE(mismatched.SetHDSeedFromHex(HexStr(otherSeed64.begin(), otherSeed64.end())));
    ASSERT_TRUE(mismatched.SetMnemonicEntropy(zeros));
    std::string phrase;
    EXPECT_FALSE(mismatched.GetMnemonicPhrase(phrase));

    // Matching entropy attached to a hex-restored wallet -> phrase available.
    CWallet matched;
    ASSERT_TRUE(matched.SetHDSeedFromHex(std::string(SEED64_HEX)));
    ASSERT_TRUE(matched.SetMnemonicEntropy(zeros));
    ASSERT_TRUE(matched.GetMnemonicPhrase(phrase));
    EXPECT_EQ(phrase, std::string(ABANDON_ART));
}
