//! KeyRing — manages multiple enrolled mnemonics with per-mnemonic derived accounts.
//!
//! The KeyRing is a metadata index over platform keychain entries. Secrets
//! (mnemonics) live in the keychain; the KeyRing holds only public data
//! (addresses, names, derivation paths).

use crate::derive;

/// Stable identifier for a keyring entry (UUID v4).
pub type KeyId = String;

/// A derivation path and its resulting address.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DerivedAccount {
    /// Derivation path: `""` for root, `"//staking"`, `"//0"`, etc.
    pub path: String,
    /// SS58 address (prefix 0, Polkadot) at this derivation path.
    pub address: String,
    /// User-assigned display name.
    pub name: Option<String>,
}

/// Public metadata for one enrolled mnemonic — no secrets.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KeyRingEntry {
    /// Stable identifier (UUID v4).
    pub id: KeyId,
    /// User-assigned name for this wallet.
    pub name: Option<String>,
    /// Root SS58 address (no derivation path).
    pub root_address: String,
    /// Derived accounts from this mnemonic (includes the root account at path `""`).
    pub derived_accounts: Vec<DerivedAccount>,
    /// Whether this entry's mnemonic is synced to iCloud Keychain.
    #[serde(default)]
    pub icloud_sync: bool,
}

/// Account descriptor compatible with Polkadot.js `InjectedAccount`.
#[derive(Debug, Clone)]
pub struct LegacyAccount {
    pub address: String,
    pub name: Option<String>,
    pub key_type: KeyType,
}

/// The cryptographic key type for a `LegacyAccount`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyType {
    Sr25519,
    Ed25519,
    Ecdsa,
}

/// Abstracts over platform keychain storage for testing.
///
/// Implement this trait to inject a mock store in unit tests so that
/// tests run without touching any real platform keychain.
pub trait KeyRingStore {
    /// Store the mnemonic in the platform keychain.
    ///
    /// `sync = true` requests iCloud Keychain sync (macOS/iOS only).
    /// Other platforms accept the parameter for API parity but ignore it.
    fn store_mnemonic(&self, id: &str, phrase: &str, sync: bool) -> Result<(), String>;
    fn load_mnemonic(&self, id: &str) -> Result<Option<zeroize::Zeroizing<String>>, String>;
    fn delete_mnemonic(&self, id: &str) -> Result<(), String>;
    /// Persist the keyring index (public metadata).
    ///
    /// `sync = true` requests iCloud Keychain sync (macOS/iOS) so a second
    /// device can discover the same account list.
    fn store_index(&self, json: &str, sync: bool) -> Result<(), String>;
    fn load_index(&self) -> Option<String>;
}

/// Production store that delegates to the platform keychain.
pub struct PlatformStore;

impl KeyRingStore for PlatformStore {
    fn store_mnemonic(&self, id: &str, phrase: &str, sync: bool) -> Result<(), String> {
        crate::keystore::store_mnemonic_for_id(id, phrase, sync).map_err(|e| e.to_string())
    }

    fn load_mnemonic(&self, id: &str) -> Result<Option<zeroize::Zeroizing<String>>, String> {
        crate::keystore::load_mnemonic_for_id(id).map_err(|e| e.to_string())
    }

    fn delete_mnemonic(&self, id: &str) -> Result<(), String> {
        crate::keystore::delete_mnemonic_for_id(id).map_err(|e| e.to_string())
    }

    fn store_index(&self, json: &str, sync: bool) -> Result<(), String> {
        crate::keystore::store_keyring_index(json, sync).map_err(|e| e.to_string())
    }

    fn load_index(&self) -> Option<String> {
        crate::keystore::load_keyring_index()
    }
}

/// Manages multiple enrolled mnemonics with derivation paths.
///
/// Secrets live in the platform keychain. The `KeyRing` holds only
/// public metadata (addresses, names, paths). Use `KeyRing<PlatformStore>`
/// in production and `KeyRing<MemoryKeyRingStore>` in tests.
pub struct KeyRing<S: KeyRingStore = PlatformStore> {
    entries: Vec<KeyRingEntry>,
    active_id: Option<KeyId>,
    /// True if any enrolled mnemonic requested iCloud sync.
    /// When set, the index is also synced so a second device sees the account list.
    icloud_sync: bool,
    store: S,
}

impl KeyRing<PlatformStore> {
    /// Create an empty keyring or load from the persisted platform index.
    pub fn new() -> Self {
        let store = PlatformStore;
        let entries: Vec<KeyRingEntry> = match store.load_index() {
            Some(json) => serde_json::from_str(&json).unwrap_or_default(),
            None => Vec::new(),
        };
        // If any entry was previously synced, keep index sync enabled.
        let icloud_sync = entries.iter().any(|e| e.icloud_sync);
        Self {
            entries,
            active_id: None,
            icloud_sync,
            store,
        }
    }
}

impl Default for KeyRing<PlatformStore> {
    fn default() -> Self {
        Self::new()
    }
}

impl<S: KeyRingStore> KeyRing<S> {
    /// Create a keyring backed by the given store (useful in tests).
    pub fn with_store(store: S) -> Self {
        let entries: Vec<KeyRingEntry> = match store.load_index() {
            Some(json) => serde_json::from_str(&json).unwrap_or_default(),
            None => Vec::new(),
        };
        let icloud_sync = entries.iter().any(|e| e.icloud_sync);
        Self {
            entries,
            active_id: None,
            icloud_sync,
            store,
        }
    }

    /// Enroll a new mnemonic. Validates the phrase, stores it in the keychain,
    /// derives the root address, and adds an entry to the index.
    ///
    /// `icloud_sync` — when `true`, requests iCloud Keychain sync on macOS/iOS
    /// by setting `kSecAttrSynchronizable`. Ignored on other platforms.
    ///
    /// Returns the new entry's stable `KeyId`.
    pub fn add(
        &mut self,
        phrase: &str,
        name: Option<String>,
        icloud_sync: bool,
    ) -> Result<KeyId, String> {
        let mnemonic =
            bip39::Mnemonic::parse(phrase).map_err(|e| format!("invalid mnemonic: {e}"))?;

        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let root_address = derive::ss58_address_with_prefix(&root.public.to_bytes(), 0);

        // C-3: Reject duplicate mnemonics — same root address means same seed.
        if self.entries.iter().any(|e| e.root_address == root_address) {
            return Err("mnemonic already enrolled (duplicate root address)".into());
        }

        let id = uuid_v4();

        self.store
            .store_mnemonic(&id, phrase, icloud_sync)
            .map_err(|e| format!("keychain store failed: {e}"))?;

        let root_account = DerivedAccount {
            path: String::new(),
            address: root_address.clone(),
            name: name.clone(),
        };

        let entry = KeyRingEntry {
            id: id.clone(),
            name,
            root_address,
            derived_accounts: vec![root_account],
            icloud_sync,
        };

        self.entries.push(entry);
        if icloud_sync {
            self.icloud_sync = true;
        }
        self.persist_index()?;

        Ok(id)
    }

    /// Add a derived account to an existing mnemonic entry.
    ///
    /// Loads the mnemonic from the keychain (triggers biometric on
    /// macOS/iOS), derives the address for `path`, and stores the
    /// derivation metadata. Returns the SS58 address of the new account.
    pub fn add_derived(
        &mut self,
        key_id: &str,
        path: &str,
        name: Option<String>,
    ) -> Result<String, String> {
        // Reject empty path — that would duplicate the root account.
        if path.is_empty() {
            return Err("use add() to enroll the root account".into());
        }

        let entry = self
            .entries
            .iter()
            .find(|e| e.id == key_id)
            .ok_or("key not found")?;

        if entry.derived_accounts.iter().any(|d| d.path == path) {
            return Err(format!("derivation path '{path}' already exists"));
        }

        let phrase = self
            .store
            .load_mnemonic(key_id)
            .map_err(|e| format!("keychain load failed: {e}"))?
            .ok_or("mnemonic not found in keychain")?;

        let mnemonic = bip39::Mnemonic::parse(phrase.as_str())
            .map_err(|e| format!("invalid stored mnemonic: {e}"))?;
        let root = derive::root_keypair_from_mnemonic(&mnemonic);

        let keypair = apply_derivation_path(&root, path)?;
        let address = derive::ss58_address_with_prefix(&keypair.public.to_bytes(), 0);

        let derived = DerivedAccount {
            path: path.to_string(),
            address: address.clone(),
            name,
        };

        // Reborrow mutably now that the immutable borrow is released.
        let entry = self
            .entries
            .iter_mut()
            .find(|e| e.id == key_id)
            .expect("entry verified above");
        entry.derived_accounts.push(derived);

        self.persist_index()?;

        Ok(address)
    }

    /// Remove a mnemonic and all its derived accounts from the keyring.
    pub fn remove(&mut self, key_id: &str) -> Result<(), String> {
        let pos = self
            .entries
            .iter()
            .position(|e| e.id == key_id)
            .ok_or("key not found")?;

        self.store
            .delete_mnemonic(key_id)
            .map_err(|e| format!("keychain delete failed: {e}"))?;

        self.entries.remove(pos);

        if self.active_id.as_deref() == Some(key_id) {
            self.active_id = None;
        }

        // Recompute — the removed entry may have been the only synced one.
        self.icloud_sync = self.entries.iter().any(|e| e.icloud_sync);

        self.persist_index()?;
        Ok(())
    }

    /// Remove a single derived account from a mnemonic entry.
    ///
    /// The root account (empty path) cannot be removed — use `remove()` instead.
    pub fn remove_derived(&mut self, key_id: &str, path: &str) -> Result<(), String> {
        if path.is_empty() {
            return Err(
                "cannot remove root account; use remove() to delete the whole entry".into(),
            );
        }

        let entry = self
            .entries
            .iter_mut()
            .find(|e| e.id == key_id)
            .ok_or("key not found")?;

        let before = entry.derived_accounts.len();
        entry.derived_accounts.retain(|d| d.path != path);

        // If nothing was removed, the path did not exist.
        if entry.derived_accounts.len() == before {
            return Err(format!("derivation path '{path}' not found"));
        }

        self.persist_index()?;
        Ok(())
    }

    /// List all enrolled entries (no secrets exposed).
    pub fn list(&self) -> &[KeyRingEntry] {
        &self.entries
    }

    /// Get the active key ID, if one has been selected.
    pub fn active_id(&self) -> Option<&str> {
        self.active_id.as_deref()
    }

    /// Load the active mnemonic from the keychain and unlock the given wallet.
    ///
    /// Returns `Err` if no active key is set, the mnemonic cannot be read
    /// from the store, or `WalletManager::load_from_mnemonic` fails.
    pub fn unlock_active(&self, wallet: &mut crate::WalletManager) -> Result<(), String> {
        let id = self.active_id.as_deref().ok_or("no active key selected")?;
        let phrase = self
            .store
            .load_mnemonic(id)
            .map_err(|e| format!("keychain load failed: {e}"))?
            .ok_or("mnemonic not found in keychain")?;
        wallet
            .load_from_mnemonic(phrase.as_str())
            .map_err(|e| e.to_string())
    }

    /// Select which enrolled key is active.
    ///
    /// Does NOT unlock the wallet — call [`unlock_active`](Self::unlock_active)
    /// to load the mnemonic into a `WalletManager`.
    pub fn set_active(&mut self, key_id: &str) -> Result<(), String> {
        if !self.entries.iter().any(|e| e.id == key_id) {
            return Err("key not found".into());
        }
        self.active_id = Some(key_id.to_string());
        Ok(())
    }

    /// Return all accounts across all mnemonics in `InjectedAccount` format.
    ///
    /// Display name resolution:
    /// - A derived account with its own name uses that name.
    /// - The root account with no name falls back to the wallet name.
    /// - Non-root accounts with no name inherit `"<wallet name> <path>"`.
    pub fn legacy_accounts(&self) -> Vec<LegacyAccount> {
        self.entries
            .iter()
            .flat_map(|entry| {
                entry.derived_accounts.iter().map(|d| {
                    let display_name = resolve_display_name(d, entry);
                    LegacyAccount {
                        address: d.address.clone(),
                        name: display_name,
                        key_type: KeyType::Sr25519,
                    }
                })
            })
            .collect()
    }

    /// Persist the entry list (public metadata only) to the keychain index.
    ///
    /// C-2 fix: When any enrolled mnemonic uses iCloud sync, the index is
    /// also synced so a second device discovers the account list.
    fn persist_index(&self) -> Result<(), String> {
        let json =
            serde_json::to_string(&self.entries).map_err(|e| format!("serialize index: {e}"))?;
        self.store
            .store_index(&json, self.icloud_sync)
            .map_err(|e| format!("persist index: {e}"))?;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Resolve the display name for a derived account, falling back to wallet name.
fn resolve_display_name(account: &DerivedAccount, entry: &KeyRingEntry) -> Option<String> {
    match (&account.name, &entry.name) {
        (Some(n), _) => Some(n.clone()),
        (None, Some(n)) if account.path.is_empty() => Some(n.clone()),
        (None, Some(n)) => Some(format!("{n} {}", account.path)),
        (None, None) => None,
    }
}

/// Apply a derivation path such as `"//staking//0"` to a root keypair.
///
/// Each `"//"` segment is a hard junction. An empty path returns the root.
pub(crate) fn apply_derivation_path(
    root: &schnorrkel::Keypair,
    path: &str,
) -> Result<schnorrkel::Keypair, String> {
    if path.is_empty() {
        return Ok(root.clone());
    }

    let junctions: Vec<&str> = path.split("//").filter(|s| !s.is_empty()).collect();

    if junctions.is_empty() {
        return Err("path contains only separators with no junction labels".into());
    }

    let mut secret = root.secret.clone();
    for junction in &junctions {
        secret = derive::hard_derive(&secret, junction.as_bytes());
    }

    Ok(secret.to_keypair())
}

/// Generate a UUID v4 using the OS random source.
fn uuid_v4() -> String {
    use rand_core::{OsRng, RngCore};
    let mut bytes = [0u8; 16];
    OsRng.fill_bytes(&mut bytes);
    // Version 4: set bits 12–15 of byte 6 to 0100.
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    // Variant 1: set bits 6–7 of byte 8 to 10.
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    format!(
        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-\
         {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0],
        bytes[1],
        bytes[2],
        bytes[3],
        bytes[4],
        bytes[5],
        bytes[6],
        bytes[7],
        bytes[8],
        bytes[9],
        bytes[10],
        bytes[11],
        bytes[12],
        bytes[13],
        bytes[14],
        bytes[15],
    )
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    use std::collections::HashMap;

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

    /// In-memory store for unit tests — no platform keychain involved.
    struct MemoryKeyRingStore {
        mnemonics: RefCell<HashMap<String, String>>,
        index: RefCell<Option<String>>,
    }

    impl MemoryKeyRingStore {
        fn new() -> Self {
            Self {
                mnemonics: RefCell::new(HashMap::new()),
                index: RefCell::new(None),
            }
        }
    }

    impl KeyRingStore for MemoryKeyRingStore {
        fn store_mnemonic(&self, id: &str, phrase: &str, _sync: bool) -> Result<(), String> {
            self.mnemonics
                .borrow_mut()
                .insert(id.to_string(), phrase.to_string());
            Ok(())
        }

        fn load_mnemonic(&self, id: &str) -> Result<Option<zeroize::Zeroizing<String>>, String> {
            Ok(self
                .mnemonics
                .borrow()
                .get(id)
                .map(|s| zeroize::Zeroizing::new(s.clone())))
        }

        fn delete_mnemonic(&self, id: &str) -> Result<(), String> {
            self.mnemonics.borrow_mut().remove(id);
            Ok(())
        }

        fn store_index(&self, json: &str, _sync: bool) -> Result<(), String> {
            *self.index.borrow_mut() = Some(json.to_string());
            Ok(())
        }

        fn load_index(&self) -> Option<String> {
            self.index.borrow().clone()
        }
    }

    fn make_keyring() -> KeyRing<MemoryKeyRingStore> {
        KeyRing::with_store(MemoryKeyRingStore::new())
    }

    #[test]
    fn test_add_creates_entry_with_root_account() {
        let mut kr = make_keyring();
        let id = kr
            .add(TEST_PHRASE, Some("My Wallet".into()), false)
            .unwrap();

        let entries = kr.list();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].id, id);
        assert_eq!(entries[0].name.as_deref(), Some("My Wallet"));
        assert_eq!(entries[0].derived_accounts.len(), 1);
        assert_eq!(entries[0].derived_accounts[0].path, "");
    }

    #[test]
    fn test_add_rejects_invalid_mnemonic() {
        let mut kr = make_keyring();
        let result = kr.add("not a valid mnemonic phrase at all", None, false);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("invalid mnemonic"));
    }

    #[test]
    fn test_add_derived_creates_new_account() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();

        let address = kr
            .add_derived(&id, "//staking", Some("Staking".into()))
            .unwrap();
        assert!(!address.is_empty());

        let entries = kr.list();
        assert_eq!(entries[0].derived_accounts.len(), 2);
        let derived = entries[0]
            .derived_accounts
            .iter()
            .find(|d| d.path == "//staking")
            .expect("staking account should exist");
        assert_eq!(derived.address, address);
        assert_eq!(derived.name.as_deref(), Some("Staking"));
    }

    #[test]
    fn test_add_derived_rejects_duplicate_path() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();

        kr.add_derived(&id, "//staking", None).unwrap();
        let result = kr.add_derived(&id, "//staking", None);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("already exists"));
    }

    #[test]
    fn test_add_derived_rejects_empty_path() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();

        let result = kr.add_derived(&id, "", None);
        assert!(result.is_err());
    }

    #[test]
    fn test_add_derived_rejects_unknown_key_id() {
        let mut kr = make_keyring();
        let result = kr.add_derived("nonexistent-id", "//staking", None);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "key not found");
    }

    #[test]
    fn test_remove_deletes_entry_and_mnemonic() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        assert_eq!(kr.list().len(), 1);

        kr.remove(&id).unwrap();
        assert_eq!(kr.list().len(), 0);
    }

    #[test]
    fn test_remove_clears_active_id() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        kr.set_active(&id).unwrap();
        assert_eq!(kr.active_id(), Some(id.as_str()));

        kr.remove(&id).unwrap();
        assert_eq!(kr.active_id(), None);
    }

    #[test]
    fn test_remove_rejects_unknown_key_id() {
        let mut kr = make_keyring();
        let result = kr.remove("nonexistent");
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "key not found");
    }

    #[test]
    fn test_remove_derived_keeps_root() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        kr.add_derived(&id, "//staking", None).unwrap();

        kr.remove_derived(&id, "//staking").unwrap();

        let entries = kr.list();
        assert_eq!(entries[0].derived_accounts.len(), 1);
        assert_eq!(entries[0].derived_accounts[0].path, "");
    }

    #[test]
    fn test_remove_derived_rejects_root_removal() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();

        let result = kr.remove_derived(&id, "");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("cannot remove root account"));
    }

    #[test]
    fn test_remove_derived_rejects_nonexistent_path() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();

        let result = kr.remove_derived(&id, "//nonexistent");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not found"));
    }

    #[test]
    fn test_set_active_rejects_unknown_id() {
        let mut kr = make_keyring();
        let result = kr.set_active("nonexistent");
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), "key not found");
    }

    #[test]
    fn test_set_active_accepts_known_id() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        kr.set_active(&id).unwrap();
        assert_eq!(kr.active_id(), Some(id.as_str()));
    }

    #[test]
    fn test_legacy_accounts_returns_all_across_entries() {
        let mut kr = make_keyring();
        let second_phrase = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong";
        kr.add(TEST_PHRASE, Some("Wallet A".into()), false).unwrap();
        let id2 = kr
            .add(second_phrase, Some("Wallet B".into()), false)
            .unwrap();
        kr.add_derived(&id2, "//staking", None).unwrap();

        // Wallet A: 1 account (root), Wallet B: 2 accounts (root + staking) = 3 total
        let accounts = kr.legacy_accounts();
        assert_eq!(accounts.len(), 3);
        assert!(accounts.iter().all(|a| a.key_type == KeyType::Sr25519));
    }

    #[test]
    fn test_legacy_accounts_display_name_resolution() {
        let mut kr = make_keyring();
        let id = kr
            .add(TEST_PHRASE, Some("My Wallet".into()), false)
            .unwrap();
        kr.add_derived(&id, "//staking", None).unwrap();

        let accounts = kr.legacy_accounts();
        // Root: inherits wallet name
        let root = accounts
            .iter()
            .find(|a| kr.list()[0].derived_accounts[0].address == a.address);
        assert_eq!(root.unwrap().name.as_deref(), Some("My Wallet"));

        // Staking: inherits wallet name + path
        let staking = accounts.iter().find(|a| {
            kr.list()[0]
                .derived_accounts
                .iter()
                .any(|d| d.path == "//staking" && d.address == a.address)
        });
        assert_eq!(
            staking.unwrap().name.as_deref(),
            Some("My Wallet //staking")
        );
    }

    #[test]
    fn test_apply_derivation_path_empty_is_root() {
        let mnemonic = bip39::Mnemonic::parse(TEST_PHRASE).unwrap();
        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let derived = apply_derivation_path(&root, "").unwrap();
        assert_eq!(root.public.to_bytes(), derived.public.to_bytes());
    }

    #[test]
    fn test_apply_derivation_path_single_junction() {
        let mnemonic = bip39::Mnemonic::parse(TEST_PHRASE).unwrap();
        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let derived = apply_derivation_path(&root, "//staking").unwrap();
        // Must differ from root
        assert_ne!(root.public.to_bytes(), derived.public.to_bytes());
    }

    #[test]
    fn test_apply_derivation_path_multiple_junctions() {
        let mnemonic = bip39::Mnemonic::parse(TEST_PHRASE).unwrap();
        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let one = apply_derivation_path(&root, "//staking").unwrap();
        let two = apply_derivation_path(&root, "//staking//0").unwrap();
        // Two junctions must differ from one junction
        assert_ne!(one.public.to_bytes(), two.public.to_bytes());
    }

    #[test]
    fn test_apply_derivation_path_deterministic() {
        let mnemonic = bip39::Mnemonic::parse(TEST_PHRASE).unwrap();
        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let a = apply_derivation_path(&root, "//staking//0").unwrap();
        let b = apply_derivation_path(&root, "//staking//0").unwrap();
        assert_eq!(a.public.to_bytes(), b.public.to_bytes());
    }

    #[test]
    fn test_apply_derivation_path_rejects_separator_only() {
        let mnemonic = bip39::Mnemonic::parse(TEST_PHRASE).unwrap();
        let root = derive::root_keypair_from_mnemonic(&mnemonic);
        let result = apply_derivation_path(&root, "//");
        assert!(result.is_err());
    }

    #[test]
    fn test_index_round_trips_through_store() {
        let store = MemoryKeyRingStore::new();
        let mut kr: KeyRing<MemoryKeyRingStore> = KeyRing::with_store(store);

        let id = kr
            .add(TEST_PHRASE, Some("Persisted".into()), false)
            .unwrap();

        // Reconstruct from the same underlying data by loading index.
        let index_json = kr.store.load_index().expect("index was persisted");
        let entries: Vec<KeyRingEntry> = serde_json::from_str(&index_json).unwrap();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].id, id);
    }

    #[test]
    fn test_root_address_is_polkadot_prefix() {
        let mut kr = make_keyring();
        kr.add(TEST_PHRASE, None, false).unwrap();
        // Prefix 0 (Polkadot) addresses start with '1'
        assert!(kr.list()[0].root_address.starts_with('1'));
    }

    // C-3: duplicate mnemonic detection
    #[test]
    fn test_add_rejects_duplicate_mnemonic() {
        let mut kr = make_keyring();
        kr.add(TEST_PHRASE, Some("First".into()), false).unwrap();
        let result = kr.add(TEST_PHRASE, Some("Second".into()), false);
        assert!(result.is_err());
        assert!(
            result.unwrap_err().contains("duplicate root address"),
            "error must mention duplicate"
        );
        // Original entry is still there.
        assert_eq!(kr.list().len(), 1);
    }

    // C-2: icloud_sync flag persisted on entries
    #[test]
    fn test_icloud_sync_flag_persisted_in_index() {
        let mut kr = make_keyring();
        kr.add(TEST_PHRASE, Some("Synced".into()), true).unwrap();

        let index_json = kr.store.load_index().unwrap();
        let entries: Vec<KeyRingEntry> = serde_json::from_str(&index_json).unwrap();
        assert!(entries[0].icloud_sync, "icloud_sync must be persisted");
    }

    #[test]
    fn test_icloud_sync_default_false_when_absent() {
        // Simulate a legacy index JSON without the icloud_sync field.
        let legacy_json = r#"[{"id":"abc","name":"Old","root_address":"1xxx","derived_accounts":[{"path":"","address":"1xxx","name":null}]}]"#;
        let entries: Vec<KeyRingEntry> = serde_json::from_str(legacy_json).unwrap();
        assert!(!entries[0].icloud_sync, "serde default must be false");
    }

    #[test]
    fn test_icloud_sync_resets_after_removing_only_synced_entry() {
        let mut kr = make_keyring();
        let synced_id = kr.add(TEST_PHRASE, Some("Synced".into()), true).unwrap();
        assert!(kr.icloud_sync, "should be true after synced add");

        kr.remove(&synced_id).unwrap();
        assert!(
            !kr.icloud_sync,
            "must reset to false after removing the only synced entry"
        );
    }

    #[test]
    fn test_icloud_sync_stays_true_if_another_synced_entry_remains() {
        let mut kr = make_keyring();
        let second_phrase = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong";
        kr.add(TEST_PHRASE, Some("Synced A".into()), true).unwrap();
        let id_b = kr
            .add(second_phrase, Some("Synced B".into()), true)
            .unwrap();
        assert!(kr.icloud_sync);

        kr.remove(&id_b).unwrap();
        assert!(
            kr.icloud_sync,
            "must stay true while another synced entry remains"
        );
    }

    // ---------------------------------------------------------------------------
    // unlock_active tests
    // ---------------------------------------------------------------------------

    #[test]
    fn test_unlock_active_loads_mnemonic_into_wallet() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        kr.set_active(&id).unwrap();

        let mut wallet = crate::WalletManager::new();
        kr.unlock_active(&mut wallet).unwrap();

        // Wallet must now be in the Unlocked state.
        assert!(
            matches!(wallet.state(), crate::WalletState::Unlocked { .. }),
            "expected WalletState::Unlocked after unlock_active"
        );
    }

    #[test]
    fn test_unlock_active_fails_without_active_key() {
        let kr = make_keyring();
        let mut wallet = crate::WalletManager::new();

        let err = kr.unlock_active(&mut wallet).unwrap_err();
        assert!(
            err.contains("no active key"),
            "expected 'no active key' in error, got: {err}"
        );
    }

    #[test]
    fn test_unlock_active_fails_with_missing_mnemonic() {
        let mut kr = make_keyring();
        let id = kr.add(TEST_PHRASE, None, false).unwrap();
        kr.store.delete_mnemonic(&id).unwrap();
        kr.set_active(&id).unwrap();

        let mut wallet = crate::WalletManager::new();
        let err = kr.unlock_active(&mut wallet).unwrap_err();
        assert!(
            err.contains("mnemonic not found"),
            "expected 'mnemonic not found' error, got: {err}"
        );
    }

    #[test]
    fn test_unlock_active_propagates_store_error() {
        // A store that always fails on load_mnemonic.
        struct FailingStore;
        impl KeyRingStore for FailingStore {
            fn store_mnemonic(&self, _: &str, _: &str, _: bool) -> Result<(), String> {
                Ok(())
            }
            fn load_mnemonic(&self, _: &str) -> Result<Option<zeroize::Zeroizing<String>>, String> {
                Err("simulated I/O failure".into())
            }
            fn delete_mnemonic(&self, _: &str) -> Result<(), String> {
                Ok(())
            }
            fn store_index(&self, _: &str, _: bool) -> Result<(), String> {
                Ok(())
            }
            fn load_index(&self) -> Option<String> {
                None
            }
        }

        let mut kr = KeyRing::with_store(FailingStore);
        // Manually inject an entry so set_active succeeds.
        kr.entries.push(KeyRingEntry {
            id: "test-id".into(),
            name: None,
            root_address: "1xxx".into(),
            derived_accounts: vec![],
            icloud_sync: false,
        });
        kr.set_active("test-id").unwrap();

        let mut wallet = crate::WalletManager::new();
        let err = kr.unlock_active(&mut wallet).unwrap_err();
        assert!(
            err.contains("keychain load failed"),
            "expected 'keychain load failed' error, got: {err}"
        );
    }
}
