//! Key derivation: mnemonic → root sr25519 keypair → per-app derived keypair.
//!
//! Uses Substrate-compatible hard derivation (`//app//<app_id>//<index>`) so that
//! a compromised child key cannot reconstruct sibling or parent keys.

use blake2::digest::{consts::U32, Digest};
use schnorrkel::{ExpansionMode, Keypair, MiniSecretKey, PublicKey, SecretKey};
use zeroize::Zeroize;

/// SS58 prefix for the "generic" Substrate network (prefix 42).
const SS58_PREFIX: u16 = 42;

/// Derive the root sr25519 keypair from a BIP-39 mnemonic.
///
/// The mnemonic is converted to a 64-byte seed via PBKDF2 (BIP-39 standard),
/// then the first 32 bytes are used as a `MiniSecretKey`.
pub fn root_keypair_from_mnemonic(mnemonic: &bip39::Mnemonic) -> Keypair {
    let mut seed = mnemonic.to_seed("");
    let mut mini_bytes = [0u8; 32];
    mini_bytes.copy_from_slice(&seed[..32]);
    seed.zeroize();
    let mini = MiniSecretKey::from_bytes(&mini_bytes).expect("32 bytes is valid");
    mini_bytes.zeroize();
    let secret = mini.expand(ExpansionMode::Ed25519);
    secret.to_keypair()
}

/// Derive a per-app keypair from the root keypair using hard derivation.
///
/// Path: `//app//<app_id>//<index>` — three hard junctions.
/// Each junction hashes the parent secret key with the junction label.
/// The index is encoded as 4 little-endian bytes, enabling multiple
/// unlinkable identities per app.
pub fn app_keypair(root: &Keypair, app_id: &str, index: u32) -> Keypair {
    let k1 = hard_derive(&root.secret, b"app");
    let k2 = hard_derive(&k1, app_id.as_bytes());
    let k3 = hard_derive(&k2, &index.to_le_bytes());
    k3.to_keypair()
}

/// Derive the SSO identity keypair at `//wallet//sso` from the root keypair.
///
/// This keypair is used as the sr25519 identity for the Polkadot App pairing
/// protocol. Its public key (account ID) is the HMAC key for topic derivation.
pub fn sso_keypair(root: &Keypair) -> Keypair {
    let k1 = hard_derive(&root.secret, b"wallet");
    let k2 = hard_derive(&k1, b"sso");
    k2.to_keypair()
}

/// Derive the wallet identity keypair at `//wallet` from the root keypair.
///
/// This keypair is used as the sr25519 signing identity for chat protocol
/// messages, matching the iOS v2 app which signs with `//wallet`.
///
/// NOTE: iOS uses `//wallet` for signing (not `//wallet//chat`). This is
/// tracked as a key-isolation issue in host-sdk#36. Once iOS is fixed to
/// use `//wallet//chat`, this function should be replaced by `chat_keypair`.
pub fn wallet_keypair(root: &Keypair) -> Keypair {
    let k1 = hard_derive(&root.secret, b"wallet");
    k1.to_keypair()
}

/// Derive the Chat encryption keypair at `//wallet//chat` from the root keypair.
///
/// This keypair provides the P-256 encryption key for chat. Its first 32 bytes
/// of the SR25519 secret are hashed via blake2b256 to produce the P-256 scalar.
///
/// NOTE: The public key of this keypair is NOT used as the chat account_id.
/// iOS uses pubkey(`//wallet`) for that. See `wallet_keypair` and host-sdk#36.
pub fn chat_keypair(root: &Keypair) -> Keypair {
    let k1 = hard_derive(&root.secret, b"wallet");
    let k2 = hard_derive(&k1, b"chat");
    k2.to_keypair()
}

/// Derive a per-product keypair from the root keypair using hard derivation.
///
/// Path: `//product//<product_id>//<index>` — three hard junctions.
/// Used by the phone to derive hard product keys on demand (Option B).
/// The host never calls this directly — it requests the derived public key
/// from the phone via ProductKeyRequest.
pub fn product_keypair(root: &Keypair, product_id: &str, index: u32) -> Keypair {
    let k1 = hard_derive(&root.secret, b"product");
    let k2 = hard_derive(&k1, product_id.as_bytes());
    let k3 = hard_derive(&k2, &index.to_le_bytes());
    k3.to_keypair()
}

/// Derive an Ed25519 signing key from the root sr25519 keypair.
///
/// Path: `//ed25519//<app_id>//<index>` — three hard junctions.
/// The 32-byte derived secret scalar is used directly as the Ed25519 seed.
pub fn ed25519_keypair_from_root(
    root: &schnorrkel::Keypair,
    app_id: &str,
    index: u32,
) -> ed25519_dalek::SigningKey {
    let k1 = hard_derive(&root.secret, b"ed25519");
    let k2 = hard_derive(&k1, app_id.as_bytes());
    let k3 = hard_derive(&k2, &index.to_le_bytes());
    // Extract the 32-byte scalar half of the expanded sr25519 secret as the Ed25519 seed.
    let expanded = k3.to_bytes();
    let mut seed = [0u8; 32];
    seed.copy_from_slice(&expanded[..32]);
    let key = ed25519_dalek::SigningKey::from_bytes(&seed);
    seed.zeroize();
    key
}

/// Derive a Substrate ECDSA (secp256k1) signing key from the root sr25519 keypair.
///
/// Path: `//ecdsa//<app_id>//<index>` — three hard junctions.
/// The 32-byte derived secret scalar is used directly as the ECDSA secret scalar seed.
pub fn substrate_ecdsa_keypair_from_root(
    root: &schnorrkel::Keypair,
    app_id: &str,
    index: u32,
) -> k256::ecdsa::SigningKey {
    let k1 = hard_derive(&root.secret, b"ecdsa");
    let k2 = hard_derive(&k1, app_id.as_bytes());
    let k3 = hard_derive(&k2, &index.to_le_bytes());
    let expanded = k3.to_bytes();
    let mut seed = [0u8; 32];
    seed.copy_from_slice(&expanded[..32]);
    // k256 requires the scalar to be non-zero and less than the curve order.
    // Hard derivation via BLAKE2b produces pseudo-random bytes; rejection is
    // astronomically unlikely but the from_bytes constructor returns an error
    // on the degenerate case — we unwrap since this is a library invariant.
    let key = k256::ecdsa::SigningKey::from_bytes((&seed).into()).expect("32-byte derived scalar");
    seed.zeroize();
    key
}

/// Substrate-compatible hard key derivation.
///
/// Computes `BLAKE2b-256(secret_key_bytes || junction_bytes)` to produce
/// a new 32-byte `MiniSecretKey`, then expands it.
pub fn hard_derive(parent: &SecretKey, junction: &[u8]) -> SecretKey {
    let mut hasher = blake2::Blake2b::<U32>::new();
    hasher.update(b"SchnsHrd"); // Substrate HDKD magic prefix
    hasher.update(parent.to_bytes());
    hasher.update(junction);
    let hash = hasher.finalize();
    let mut mini_bytes = [0u8; 32];
    mini_bytes.copy_from_slice(&hash);
    let mini = MiniSecretKey::from_bytes(&mini_bytes).expect("32 bytes is valid");
    mini_bytes.zeroize();
    mini.expand(ExpansionMode::Ed25519)
}

/// Encode an sr25519 public key as an SS58 address string.
///
/// Format: `base58check(prefix || pubkey || checksum[0..2])`
/// where checksum = `BLAKE2b-512(b"SS58PRE" || prefix || pubkey)`.
pub fn ss58_address(pubkey: &PublicKey) -> String {
    ss58_address_with_prefix(&pubkey.to_bytes(), SS58_PREFIX)
}

/// Encode a 32-byte public key as an SS58 address with a specific network prefix.
///
/// Common prefixes: 0 = Polkadot, 2 = Kusama, 42 = generic Substrate, 172 = Phala.
///
/// Encoding per the SS58 spec:
/// - Prefixes 0–63: encoded as a single byte equal to the prefix value.
/// - Prefixes 64–16383: encoded as two bytes using the "canary" scheme so that
///   the high-bit pattern `0b01xxxxxx` distinguishes them from single-byte prefixes.
pub fn ss58_address_with_prefix(pubkey_bytes: &[u8; 32], prefix: u16) -> String {
    // Single-byte path: 2 prefix bytes (canary) + 32 key + 2 checksum = 36
    let capacity = if prefix < 64 { 35 } else { 36 };
    let mut payload = Vec::with_capacity(capacity);

    if prefix < 64 {
        // Single-byte encoding: prefix fits directly in one byte.
        payload.push(prefix as u8);
    } else {
        // Two-byte "canary" encoding for prefixes 64–16383.
        // byte0 bits: 01xxxxxx where xxxxxx = prefix[7:2]
        // byte1 bits: ppxxxxxx where pp = prefix[1:0] and xxxxxx = prefix[13:8]
        let byte0 = (((prefix & 0b1111_1100) >> 2) | 0b0100_0000) as u8;
        let byte1 = ((prefix >> 8) | ((prefix & 0b11) << 6)) as u8;
        payload.push(byte0);
        payload.push(byte1);
    }

    payload.extend_from_slice(pubkey_bytes);

    // Checksum: BLAKE2b-512("SS58PRE" || prefix_bytes || pubkey)[0..2]
    let mut hasher = blake2::Blake2b::<blake2::digest::consts::U64>::new();
    hasher.update(b"SS58PRE");
    hasher.update(&payload);
    let checksum = hasher.finalize();
    payload.extend_from_slice(&checksum[..2]);

    bs58::encode(payload).into_string()
}

// ---------------------------------------------------------------------------
// secp256k1 / BIP-44 derivation (Ethereum + Bitcoin)
// ---------------------------------------------------------------------------

pub mod secp256k1 {
    use bip32::{DerivationPath, XPrv};
    use k256::ecdsa::SigningKey;
    use sha3::{Digest as Sha3Digest, Keccak256};

    /// BIP-44 ETH derivation path: m/44'/60'/0'/0/0
    const ETH_PATH: &str = "m/44'/60'/0'/0/0";
    /// BIP-84 BTC derivation path for P2WPKH (native SegWit): m/84'/0'/0'/0/0
    const BTC_PATH: &str = "m/84'/0'/0'/0/0";

    /// Derive a secp256k1 signing key from a BIP-39 mnemonic using a BIP-44 path.
    fn derive_key(mnemonic: &bip39::Mnemonic, path: &str) -> SigningKey {
        use zeroize::Zeroize;
        let mut seed = mnemonic.to_seed("");
        let dp: DerivationPath = path.parse().expect("valid BIP-44 path");
        let child = XPrv::derive_from_path(seed, &dp).expect("valid derivation");
        seed.zeroize();
        child.into()
    }

    /// Derive the ETH secp256k1 signing key (m/44'/60'/0'/0/0).
    pub fn eth_key(mnemonic: &bip39::Mnemonic) -> SigningKey {
        derive_key(mnemonic, ETH_PATH)
    }

    /// Derive the BTC secp256k1 signing key (BIP-84: m/84'/0'/0'/0/0).
    pub fn btc_key(mnemonic: &bip39::Mnemonic) -> SigningKey {
        derive_key(mnemonic, BTC_PATH)
    }

    /// EIP-55 checksummed Ethereum address from a signing key.
    /// Format: `0x` + 40 hex chars with mixed-case checksum.
    pub fn eth_address(key: &SigningKey) -> String {
        use k256::ecdsa::VerifyingKey;
        let vk = VerifyingKey::from(key);
        eth_address_from_verifying_key(&vk)
    }

    /// EIP-55 checksummed Ethereum address from a verifying (public) key.
    /// Used for signature recovery verification.
    pub fn eth_address_from_verifying_key(vk: &k256::ecdsa::VerifyingKey) -> String {
        // Uncompressed public key (65 bytes: 0x04 || x || y), skip the 0x04 prefix
        let pubkey_bytes = vk.to_encoded_point(false);
        let pubkey_uncompressed = &pubkey_bytes.as_bytes()[1..]; // 64 bytes

        let hash = Keccak256::digest(pubkey_uncompressed);
        let addr_bytes = &hash[12..]; // last 20 bytes

        // EIP-55 checksum
        let hex_lower: String = addr_bytes.iter().map(|b| format!("{b:02x}")).collect();
        let checksum_hash = Keccak256::digest(hex_lower.as_bytes());

        let mut addr = String::with_capacity(42);
        addr.push_str("0x");
        for (i, c) in hex_lower.chars().enumerate() {
            let nibble = (checksum_hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 })) & 0xf;
            if nibble >= 8 {
                addr.push(c.to_ascii_uppercase());
            } else {
                addr.push(c);
            }
        }
        addr
    }

    /// P2WPKH (bech32) Bitcoin mainnet address from a signing key.
    /// Mainnet only (HRP "bc"). Testnet would use HRP "tb" and path m/44'/1'/0'/0/0.
    pub fn btc_address_p2wpkh(key: &SigningKey) -> String {
        use bech32::{segwit, Hrp};
        use k256::ecdsa::VerifyingKey;
        use ripemd::Ripemd160;
        use sha2::{Digest as Sha2Digest, Sha256};

        let vk = VerifyingKey::from(key);
        let compressed = vk.to_encoded_point(true);
        let compressed_bytes = compressed.as_bytes(); // 33 bytes

        // HASH160 = RIPEMD160(SHA256(compressed_pubkey))
        let sha = Sha256::digest(compressed_bytes);
        let hash160 = Ripemd160::digest(sha);

        // Witness version 0, 20-byte program
        segwit::encode(Hrp::parse("bc").unwrap(), segwit::VERSION_0, &hash160)
            .expect("valid witness program")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEST_MNEMONIC: &str =
        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

    #[test]
    fn deterministic_chat_derivation() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let chat1 = chat_keypair(&root);
        let chat2 = chat_keypair(&root);

        // Deterministic
        assert_eq!(chat1.public.to_bytes(), chat2.public.to_bytes());
        // Differs from SSO (sibling under //wallet)
        let sso = sso_keypair(&root);
        assert_ne!(chat1.public.to_bytes(), sso.public.to_bytes());
        // Differs from root
        assert_ne!(chat1.public.to_bytes(), root.public.to_bytes());
        // Differs from //app//chat//0 (different derivation tree)
        let app_chat = app_keypair(&root, "chat", 0);
        assert_ne!(chat1.public.to_bytes(), app_chat.public.to_bytes());
    }

    #[test]
    fn deterministic_root_derivation() {
        let m1 = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let kp1 = root_keypair_from_mnemonic(&m1);
        let kp2 = root_keypair_from_mnemonic(&m1);
        assert_eq!(kp1.public.to_bytes(), kp2.public.to_bytes());
    }

    #[test]
    fn deterministic_app_derivation() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let app1a = app_keypair(&root, "com.test.app1", 0);
        let app1b = app_keypair(&root, "com.test.app1", 0);
        let app2 = app_keypair(&root, "com.test.app2", 0);

        // Same app_id + index → same key
        assert_eq!(app1a.public.to_bytes(), app1b.public.to_bytes());
        // Different app_id → different key
        assert_ne!(app1a.public.to_bytes(), app2.public.to_bytes());
        // App key differs from root
        assert_ne!(app1a.public.to_bytes(), root.public.to_bytes());
    }

    #[test]
    fn different_index_produces_different_key() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let idx0 = app_keypair(&root, "com.test.app", 0);
        let idx1 = app_keypair(&root, "com.test.app", 1);
        let idx_max = app_keypair(&root, "com.test.app", u32::MAX);

        // Different indices → different keys
        assert_ne!(idx0.public.to_bytes(), idx1.public.to_bytes());
        assert_ne!(idx0.public.to_bytes(), idx_max.public.to_bytes());
        assert_ne!(idx1.public.to_bytes(), idx_max.public.to_bytes());
    }

    #[test]
    fn new_path_differs_from_old_host_app_path() {
        // The old derivation was //host//app//<app_id> (3 junctions, no index).
        // Verify the new //app//<app_id>//<index> path produces different keys,
        // preventing accidental regression to the old scheme.
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);

        // Old path (inline, not exposed publicly)
        let old_k1 = hard_derive(&root.secret, b"host");
        let old_k2 = hard_derive(&old_k1, b"app");
        let old_k3 = hard_derive(&old_k2, b"com.test.app");
        let old_key = old_k3.to_keypair();

        // New path
        let new_key = app_keypair(&root, "com.test.app", 0);

        assert_ne!(
            old_key.public.to_bytes(),
            new_key.public.to_bytes(),
            "New derivation path must differ from old //host//app path"
        );
    }

    #[test]
    fn ss58_address_format() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let addr = ss58_address(&root.public);
        // SS58 addresses start with '5' for prefix 42
        assert!(addr.starts_with('5'), "got: {addr}");
        // Should be 47-48 chars
        assert!(addr.len() >= 46 && addr.len() <= 50, "len: {}", addr.len());
    }

    #[test]
    fn ss58_polkadot_prefix() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let addr = ss58_address_with_prefix(&root.public.to_bytes(), 0);
        // Polkadot addresses start with '1'
        assert!(
            addr.starts_with('1'),
            "Polkadot prefix 0 should start with '1', got: {addr}"
        );
        // Decode and verify prefix byte
        let decoded = bs58::decode(&addr).into_vec().unwrap();
        assert_eq!(decoded[0], 0); // Polkadot prefix
        assert_eq!(&decoded[1..33], &root.public.to_bytes());
    }

    #[test]
    fn ss58_roundtrip_valid() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let addr = ss58_address(&root.public);
        // Decode and verify prefix + pubkey
        let decoded = bs58::decode(&addr).into_vec().unwrap();
        assert_eq!(decoded[0], 42); // generic prefix
        assert_eq!(&decoded[1..33], &root.public.to_bytes());
    }

    #[test]
    fn ss58_two_byte_canary_encoding_prefix_172() {
        // Prefix 172 (Phala network) requires the two-byte "canary" encoding.
        // Verify the prefix bytes decode to the canary pattern specified by SS58:
        //   byte0 = ((prefix & 0b11111100) >> 2) | 0b01000000
        //   byte1 = (prefix >> 8) | ((prefix & 0b11) << 6)
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let root = root_keypair_from_mnemonic(&m);
        let addr = ss58_address_with_prefix(&root.public.to_bytes(), 172);

        let decoded = bs58::decode(&addr).into_vec().unwrap();

        // Verify the canary byte pattern for prefix 172
        let expected_byte0 = (((172u16 & 0b1111_1100) >> 2) | 0b0100_0000) as u8;
        let expected_byte1 = ((172u16 >> 8) | ((172u16 & 0b11) << 6)) as u8;
        assert_eq!(decoded[0], expected_byte0, "canary byte0 mismatch");
        assert_eq!(decoded[1], expected_byte1, "canary byte1 mismatch");

        // Recover the original prefix from the canary bytes
        let recovered = ((decoded[0] as u16 & 0b0011_1111) << 2)
            | ((decoded[1] as u16 & 0b1100_0000) >> 6)
            | ((decoded[1] as u16 & 0b0011_1111) << 8);
        assert_eq!(recovered, 172, "recovered prefix must equal 172");

        // Public key sits at bytes 2..34 for two-byte-prefix addresses
        assert_eq!(&decoded[2..34], &root.public.to_bytes());
    }

    // ---- secp256k1 / BIP-44 tests ----

    #[test]
    fn eth_key_deterministic() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let k1 = secp256k1::eth_key(&m);
        let k2 = secp256k1::eth_key(&m);
        assert_eq!(k1.to_bytes(), k2.to_bytes());
    }

    #[test]
    fn eth_address_known_vector() {
        // "abandon x11 about" mnemonic, m/44'/60'/0'/0/0 should produce this address.
        // Verified against MetaMask and iancoleman.io/bip39
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let key = secp256k1::eth_key(&m);
        let addr = secp256k1::eth_address(&key);
        assert!(
            addr.starts_with("0x"),
            "ETH address must start with 0x, got: {addr}"
        );
        assert_eq!(
            addr.len(),
            42,
            "ETH address must be 42 chars, got: {}",
            addr.len()
        );
        // Known address for this mnemonic — exact EIP-55 checksummed form
        assert_eq!(
            addr, "0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
            "ETH address mismatch (exact EIP-55 checksum)"
        );
    }

    #[test]
    fn eth_address_eip55_checksum() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let key = secp256k1::eth_key(&m);
        let addr = secp256k1::eth_address(&key);
        // Must have mixed case (EIP-55 checksum), not all lower
        let hex_part = &addr[2..];
        let has_upper = hex_part.chars().any(|c| c.is_ascii_uppercase());
        let has_lower = hex_part.chars().any(|c| c.is_ascii_lowercase());
        assert!(
            has_upper && has_lower,
            "EIP-55 should produce mixed case: {addr}"
        );
    }

    #[test]
    fn btc_key_deterministic() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let k1 = secp256k1::btc_key(&m);
        let k2 = secp256k1::btc_key(&m);
        assert_eq!(k1.to_bytes(), k2.to_bytes());
    }

    #[test]
    fn btc_address_format() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let key = secp256k1::btc_key(&m);
        let addr = secp256k1::btc_address_p2wpkh(&key);
        // P2WPKH bech32 addresses start with "bc1q"
        assert!(
            addr.starts_with("bc1q"),
            "BTC P2WPKH should start with bc1q, got: {addr}"
        );
        // 42-62 chars for bech32
        assert!(
            addr.len() >= 42 && addr.len() <= 62,
            "BTC addr len: {}",
            addr.len()
        );
    }

    #[test]
    fn btc_address_known_vector() {
        // BIP-84 spec test vector: "abandon x11 about" at m/84'/0'/0'/0/0
        // From https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let key = secp256k1::btc_key(&m);
        let addr = secp256k1::btc_address_p2wpkh(&key);
        assert_eq!(addr, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
    }

    #[test]
    fn eth_and_btc_keys_differ() {
        let m = bip39::Mnemonic::parse(
            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
        ).unwrap();
        let eth = secp256k1::eth_key(&m);
        let btc = secp256k1::btc_key(&m);
        assert_ne!(
            eth.to_bytes(),
            btc.to_bytes(),
            "ETH and BTC keys must differ (different derivation paths)"
        );
    }

    // ---- product_keypair tests ----

    #[test]
    fn test_product_keypair_deterministic() {
        let root = root_keypair_from_mnemonic(&bip39::Mnemonic::parse(TEST_MNEMONIC).unwrap());
        let kp1 = product_keypair(&root, "acme.dot", 0);
        let kp2 = product_keypair(&root, "acme.dot", 0);
        assert_eq!(kp1.public.to_bytes(), kp2.public.to_bytes());
    }

    #[test]
    fn test_product_keypair_different_products_differ() {
        let root = root_keypair_from_mnemonic(&bip39::Mnemonic::parse(TEST_MNEMONIC).unwrap());
        let kp1 = product_keypair(&root, "acme.dot", 0);
        let kp2 = product_keypair(&root, "other.dot", 0);
        assert_ne!(kp1.public.to_bytes(), kp2.public.to_bytes());
    }

    #[test]
    fn test_product_keypair_different_indices_differ() {
        let root = root_keypair_from_mnemonic(&bip39::Mnemonic::parse(TEST_MNEMONIC).unwrap());
        let kp1 = product_keypair(&root, "acme.dot", 0);
        let kp2 = product_keypair(&root, "acme.dot", 1);
        assert_ne!(kp1.public.to_bytes(), kp2.public.to_bytes());
    }

    #[test]
    fn test_product_keypair_differs_from_app_keypair() {
        let root = root_keypair_from_mnemonic(&bip39::Mnemonic::parse(TEST_MNEMONIC).unwrap());
        let product = product_keypair(&root, "acme.dot", 0);
        let app = app_keypair(&root, "acme.dot", 0);
        assert_ne!(
            product.public.to_bytes(),
            app.public.to_bytes(),
            "product and app paths must produce different keys"
        );
    }
}
