use std::collections::HashMap;
use std::sync::Once;

use nwep::merkle::{LogStorage, MerkleEntry, MerkleEntryType, MerkleLog, MerkleProof, merkle_node_hash};
use nwep::logindex::{LogIndex, LogIndexEntry, LogIndexStorage};
use nwep::*;

// ─── Init ────────────────────────────────────────────────────────────────────

static INIT: Once = Once::new();
fn init() {
    INIT.call_once(|| nwep::init().expect("nwep::init failed"));
}

// ─── In-memory storage helpers ───────────────────────────────────────────────

struct MemLog(Vec<Vec<u8>>);

impl LogStorage for MemLog {
    fn append(&mut self, _index: u64, entry: &[u8]) -> Result<(), Error> {
        self.0.push(entry.to_vec());
        Ok(())
    }
    fn get(&self, index: u64, buf: &mut [u8]) -> Result<usize, Error> {
        let data = &self.0[index as usize];
        let n = data.len().min(buf.len());
        buf[..n].copy_from_slice(&data[..n]);
        Ok(n)
    }
    fn size(&self) -> u64 { self.0.len() as u64 }
}

struct MemIndex(HashMap<[u8; 32], LogIndexEntry>);

impl LogIndexStorage for MemIndex {
    fn get(&self, node_id: &NodeId) -> Result<LogIndexEntry, Error> {
        self.0.get(&node_id.0)
            .cloned()
            .ok_or_else(|| Error::from_code(nwep::error::ERR_STORAGE_FILE_NOT_FOUND))
    }
    fn put(&mut self, entry: &LogIndexEntry) -> Result<(), Error> {
        self.0.insert(entry.node_id.0, entry.clone());
        Ok(())
    }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

fn now_ns() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64
}

fn make_entry(kp: &Keypair, ts: u64) -> MerkleEntry {
    MerkleEntry {
        entry_type: MerkleEntryType::KeyBinding,
        timestamp: ts,
        node_id: kp.node_id().unwrap(),
        pubkey: kp.public_key(),
        prev_pubkey: [0u8; 32],
        recovery_pubkey: [0u8; 32],
        signature: [0u8; 64],
    }
}

// ─── Init / version ──────────────────────────────────────────────────────────

#[test]
fn test_init_and_version() {
    init();
    let v = nwep::version();
    assert!(!v.is_empty(), "version should not be empty");
}

// ─── Keypair ─────────────────────────────────────────────────────────────────

#[test]
fn test_keypair_generate() {
    init();
    let kp = Keypair::generate().unwrap();
    assert_ne!(kp.public_key(), [0u8; 32], "pubkey should not be zero");
}

#[test]
fn test_keypair_from_seed_deterministic() {
    init();
    let seed = [42u8; 32];
    let kp1 = Keypair::from_seed(&seed).unwrap();
    let kp2 = Keypair::from_seed(&seed).unwrap();
    assert_eq!(kp1.public_key(), kp2.public_key());
    assert_eq!(kp1.seed(), seed);
}

#[test]
fn test_keypair_from_privkey_roundtrip() {
    init();
    let kp1 = Keypair::generate().unwrap();
    let kp2 = Keypair::from_privkey(&kp1.private_key()).unwrap();
    assert_eq!(kp1.public_key(), kp2.public_key());
}

#[test]
fn test_node_id() {
    init();
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    assert_ne!(nid.0, [0u8; 32]);
    assert_eq!(format!("{nid}").len(), 64, "hex should be 64 chars");

    // node_id_from_pubkey matches keypair.node_id()
    let nid2 = nwep::keypair::node_id_from_pubkey(&kp.public_key()).unwrap();
    assert_eq!(nid, nid2);

    // node_id_eq
    assert!(nwep::keypair::node_id_eq(&nid, &nid2));
    let kp2 = Keypair::generate().unwrap();
    let nid3 = kp2.node_id().unwrap();
    assert!(!nwep::keypair::node_id_eq(&nid, &nid3));
}

// ─── ManagedIdentity ─────────────────────────────────────────────────────────

#[test]
fn test_managed_identity_new() {
    init();
    let kp = Keypair::generate().unwrap();
    let mi = nwep::keypair::ManagedIdentity::new(&kp, None, now_ns()).unwrap();
    assert!(!mi.is_revoked());
    assert_ne!(mi.node_id().0, [0u8; 32]);
    assert!(mi.active_keypair().is_some());
    assert_eq!(mi.active_keys().len(), 1);
}

#[test]
fn test_managed_identity_rotation() {
    init();
    let kp = Keypair::generate().unwrap();
    let now = now_ns();
    let mut mi = nwep::keypair::ManagedIdentity::new(&kp, None, now).unwrap();
    let pk_before = mi.active_pubkey().unwrap();

    mi.rotate(now + 1_000_000).unwrap();

    // During the overlap period both keys should be active
    assert_eq!(mi.active_keys().len(), 2);
    let pk_after = mi.active_pubkey().unwrap();
    assert_ne!(pk_before, pk_after, "pubkey should change after rotation");
}

#[test]
fn test_managed_identity_update() {
    init();
    let kp = Keypair::generate().unwrap();
    let now = now_ns();
    let mut mi = nwep::keypair::ManagedIdentity::new(&kp, None, now).unwrap();
    // update() should not panic
    mi.update(now + 60_000_000_000);
}

#[test]
fn test_recovery_authority() {
    init();
    let ra = nwep::keypair::RecoveryAuthority::new().unwrap();
    let pk = ra.public_key().expect("RA should have a public key");
    assert_ne!(pk, [0u8; 32]);

    // from_keypair
    let kp = Keypair::generate().unwrap();
    let ra2 = nwep::keypair::RecoveryAuthority::from_keypair(&kp).unwrap();
    assert!(ra2.public_key().is_some());
}

#[test]
fn test_managed_identity_revoke() {
    init();
    let kp = Keypair::generate().unwrap();
    let now = now_ns();
    let ra = nwep::keypair::RecoveryAuthority::new().unwrap();
    let mut mi = nwep::keypair::ManagedIdentity::new(&kp, Some(&ra), now).unwrap();
    assert!(!mi.is_revoked());
    mi.revoke(&ra, now + 1_000_000).unwrap();
    assert!(mi.is_revoked());
}

// ─── Ed25519 crypto ───────────────────────────────────────────────────────────

#[test]
fn test_sign_verify() {
    init();
    let kp = Keypair::generate().unwrap();
    let msg = b"hello nwep";
    let sig = nwep::crypto::sign(&kp, msg).unwrap();
    nwep::crypto::verify(&kp.public_key(), &sig, msg).unwrap();
    // tampered message
    assert!(nwep::crypto::verify(&kp.public_key(), &sig, b"other").is_err());
    // wrong pubkey
    let kp2 = Keypair::generate().unwrap();
    assert!(nwep::crypto::verify(&kp2.public_key(), &sig, msg).is_err());
}

#[test]
fn test_sign_empty_message() {
    init();
    let kp = Keypair::generate().unwrap();
    let sig = nwep::crypto::sign(&kp, b"").unwrap();
    nwep::crypto::verify(&kp.public_key(), &sig, b"").unwrap();
}

#[test]
fn test_challenge() {
    init();
    let kp = Keypair::generate().unwrap();
    let c = nwep::crypto::challenge_generate().unwrap();
    assert_ne!(c, [0u8; 32]);
    let resp = nwep::crypto::challenge_sign(&c, &kp).unwrap();
    nwep::crypto::challenge_verify(&resp, &c, &kp.public_key()).unwrap();
    // wrong challenge
    let c2 = nwep::crypto::challenge_generate().unwrap();
    assert!(nwep::crypto::challenge_verify(&resp, &c2, &kp.public_key()).is_err());
}

#[test]
fn test_random_bytes() {
    init();
    let mut buf = [0u8; 64];
    nwep::crypto::random_bytes(&mut buf).unwrap();
    assert_ne!(&buf[..], &[0u8; 64][..]);
    // empty is fine
    nwep::crypto::random_bytes(&mut []).unwrap();
}

#[test]
fn test_shamir_split_combine() {
    init();
    let secret = [0x77u8; 32];
    let shares = nwep::crypto::shamir_split(&secret, 5, 3).unwrap();
    assert_eq!(shares.len(), 5);
    // any 3 shares suffice
    let r1 = nwep::crypto::shamir_combine(&shares[..3]).unwrap();
    assert_eq!(r1, secret);
    let r2 = nwep::crypto::shamir_combine(&shares[2..]).unwrap();
    assert_eq!(r2, secret);
    // first-and-last 3
    let subset = vec![shares[0].clone(), shares[2].clone(), shares[4].clone()];
    let r3 = nwep::crypto::shamir_combine(&subset).unwrap();
    assert_eq!(r3, secret);
}

// ─── Encoding ────────────────────────────────────────────────────────────────

#[test]
fn test_base58_roundtrip() {
    init();
    let data = [0xABu8; 32];
    let enc = nwep::encoding::base58_encode(&data);
    assert!(!enc.is_empty());
    let dec = nwep::encoding::base58_decode(&enc).unwrap();
    assert_eq!(dec, data);
}

#[test]
fn test_base64_roundtrip() {
    init();
    let data = b"hello nwep world 1234";
    let enc = nwep::encoding::base64_encode(data);
    assert!(!enc.is_empty());
    let dec = nwep::encoding::base64_decode(&enc).unwrap();
    assert_eq!(dec.as_slice(), data.as_slice());
}

#[test]
fn test_base64_decode_n() {
    init();
    let data = b"test bytes for base64n";
    let enc = nwep::encoding::base64_encode(data);
    let dec = nwep::encoding::base64_decode_n(enc.as_bytes()).unwrap();
    assert_eq!(dec.as_slice(), data.as_slice());
}

// ─── Addr / URL ──────────────────────────────────────────────────────────────

#[test]
fn test_addr_encode_decode() {
    init();
    use std::net::Ipv4Addr;
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let addr = Addr::new_ipv4(Ipv4Addr::LOCALHOST, nid, 6937);
    let enc = addr.encode().unwrap();
    let dec = Addr::decode(&enc).unwrap();
    assert_eq!(dec.node_id, nid);
    // port is not preserved in Addr encode/decode (C library returns NWEP_DEFAULT_PORT)
}

#[test]
fn test_addr_ipv6() {
    init();
    use std::net::Ipv6Addr;
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let addr = Addr::new_ipv6(Ipv6Addr::LOCALHOST, nid, 9000);
    let enc = addr.encode().unwrap();
    let dec = Addr::decode(&enc).unwrap();
    assert_eq!(dec.node_id, nid);
    // port is not preserved in Addr encode/decode (C library returns NWEP_DEFAULT_PORT)
}

#[test]
fn test_url_parse_format() {
    init();
    use std::net::Ipv4Addr;
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let addr = Addr::new_ipv4(Ipv4Addr::LOCALHOST, nid, 6937);
    let url = Url { addr, path: "/hello".to_string() };
    let s = url.format().unwrap();
    assert!(s.starts_with("web://"));
    assert!(s.contains("/hello"));

    let parsed = Url::parse(&s).unwrap();
    assert_eq!(parsed.path, "/hello");
    assert_eq!(parsed.addr.port, 6937);
    assert_eq!(parsed.addr.node_id, nid);
}

// ─── Protocol helpers ────────────────────────────────────────────────────────

#[test]
fn test_method_validation() {
    init();
    use nwep::protocol::*;
    assert!(method_is_valid(METHOD_READ));
    assert!(method_is_valid(METHOD_WRITE));
    assert!(method_is_valid(METHOD_UPDATE));
    assert!(method_is_valid(METHOD_DELETE));
    assert!(!method_is_valid("BOGUS"));
    assert!(method_is_idempotent(METHOD_READ));
    assert!(!method_is_idempotent(METHOD_WRITE));
    assert!(method_allowed_0rtt(METHOD_READ));
}

#[test]
fn test_status_validation() {
    init();
    use nwep::protocol::*;
    assert!(status_is_valid(STATUS_OK));
    assert!(status_is_success(STATUS_OK));
    assert!(!status_is_error(STATUS_OK));
    assert!(status_is_error(STATUS_NOT_FOUND));
    assert!(status_is_error(STATUS_INTERNAL_ERROR));
    assert!(!status_is_valid("garbage"));
}

#[test]
fn test_id_generation_uniqueness() {
    init();
    let t1 = nwep::protocol::trace_id_generate().unwrap();
    let t2 = nwep::protocol::trace_id_generate().unwrap();
    assert_ne!(t1, t2);
    assert_ne!(t1, [0u8; 16]);

    let r1 = nwep::protocol::request_id_generate().unwrap();
    let r2 = nwep::protocol::request_id_generate().unwrap();
    assert_ne!(r1, r2);
}

// ─── Error system ────────────────────────────────────────────────────────────

#[test]
fn test_error_from_code() {
    init();
    use nwep::error::*;
    let e = Error::from_code(ERR_NETWORK_CONN_FAILED);
    assert_eq!(e.code, ERR_NETWORK_CONN_FAILED);
    assert!(!e.message.is_empty());
    assert!(matches!(e.category, ErrorCategory::Network));

    let e2 = Error::from_code(ERR_CRYPTO_VERIFY_FAILED);
    assert!(matches!(e2.category, ErrorCategory::Crypto));

    let e3 = Error::from_code(ERR_PROTO_PATH_NOT_FOUND);
    assert!(matches!(e3.category, ErrorCategory::Protocol));
}

#[test]
fn test_err_to_status() {
    init();
    let s = nwep::protocol::err_to_status(nwep::error::ERR_PROTO_PATH_NOT_FOUND);
    assert!(!s.is_empty());
    let s2 = nwep::protocol::err_to_status(nwep::error::ERR_PROTO_UNAUTHORIZED);
    assert!(!s2.is_empty());
}

// ─── BLS ─────────────────────────────────────────────────────────────────────

#[test]
fn test_bls_sign_verify() {
    init();
    use nwep::bls::*;
    let kp = BlsKeypair::generate().unwrap();
    let msg = b"bls test message";
    let sig = bls_sign(&kp, msg).unwrap();
    bls_verify(&kp.pubkey(), &sig, msg).unwrap();
    assert!(bls_verify(&kp.pubkey(), &sig, b"wrong").is_err());
}

#[test]
fn test_bls_aggregate() {
    init();
    use nwep::bls::*;
    let msg = b"aggregate";
    let kp1 = BlsKeypair::generate().unwrap();
    let kp2 = BlsKeypair::generate().unwrap();
    let kp3 = BlsKeypair::generate().unwrap();
    let sig1 = bls_sign(&kp1, msg).unwrap();
    let sig2 = bls_sign(&kp2, msg).unwrap();
    let sig3 = bls_sign(&kp3, msg).unwrap();
    let agg = bls_aggregate_sigs(&[sig1, sig2, sig3]).unwrap();
    bls_verify_aggregate(&[kp1.pubkey(), kp2.pubkey(), kp3.pubkey()], &agg, msg).unwrap();
}

#[test]
fn test_bls_pubkey_serialization() {
    init();
    use nwep::bls::*;
    let kp = BlsKeypair::generate().unwrap();
    let pk = kp.pubkey();
    let bytes = bls_pubkey_serialize(&pk).unwrap();
    let pk2 = bls_pubkey_deserialize(&bytes).unwrap();
    assert_eq!(pk, pk2);
}

#[test]
fn test_bls_from_seed_deterministic() {
    init();
    use nwep::bls::*;
    let ikm = b"deterministic-ikm-for-bls-keypair";
    let kp1 = BlsKeypair::from_seed(ikm).unwrap();
    let kp2 = BlsKeypair::from_seed(ikm).unwrap();
    assert_eq!(kp1.pubkey(), kp2.pubkey());
}

// ─── MerkleLog ───────────────────────────────────────────────────────────────

#[test]
fn test_merkle_log_append_get() {
    init();
    let mut log = MerkleLog::new(MemLog(vec![])).unwrap();
    assert_eq!(log.size(), 0);
    let kp = Keypair::generate().unwrap();
    let entry = make_entry(&kp, 1_000_000);
    let idx = log.append(&entry).unwrap();
    assert_eq!(idx, 0);
    assert_eq!(log.size(), 1);
    let got = log.get(0).unwrap();
    assert_eq!(got.node_id, entry.node_id);
    assert_eq!(got.pubkey, entry.pubkey);
}

#[test]
fn test_merkle_log_root_and_proof() {
    init();
    let mut log = MerkleLog::new(MemLog(vec![])).unwrap();
    let now = now_ns();
    for i in 0..8u64 {
        let kp = Keypair::generate().unwrap();
        log.append(&make_entry(&kp, now + i)).unwrap();
    }
    assert_eq!(log.size(), 8);
    let root = log.root().unwrap();
    assert_ne!(root.0, [0u8; 32]);

    let proof = log.prove(3).unwrap();
    assert_eq!(proof.index, 3);
    proof.verify(&root).unwrap();
}

#[test]
fn test_merkle_proof_encode_decode() {
    init();
    let mut log = MerkleLog::new(MemLog(vec![])).unwrap();
    let kp = Keypair::generate().unwrap();
    log.append(&make_entry(&kp, now_ns())).unwrap();
    let root = log.root().unwrap();
    let proof = log.prove(0).unwrap();

    let enc = proof.encode().unwrap();
    let dec = MerkleProof::decode(&enc).unwrap();
    assert_eq!(dec.index, proof.index);
    assert_eq!(dec.log_size, proof.log_size);
    dec.verify(&root).unwrap();
}

#[test]
fn test_merkle_entry_encode_decode() {
    init();
    let kp = Keypair::generate().unwrap();
    let entry = make_entry(&kp, 999_000_000);
    let enc = entry.encode().unwrap();
    let dec = MerkleEntry::decode(&enc).unwrap();
    assert_eq!(dec.entry_type, entry.entry_type);
    assert_eq!(dec.node_id, entry.node_id);
    assert_eq!(dec.pubkey, entry.pubkey);
    assert_eq!(dec.timestamp, entry.timestamp);
}

#[test]
fn test_merkle_leaf_hash_deterministic() {
    init();
    let kp = Keypair::generate().unwrap();
    let entry = make_entry(&kp, now_ns());
    let h1 = entry.leaf_hash().unwrap();
    let h2 = entry.leaf_hash().unwrap();
    assert_eq!(h1, h2);
    assert_ne!(h1.0, [0u8; 32]);
}

#[test]
fn test_merkle_node_hash() {
    init();
    let l = MerkleHash([1u8; 32]);
    let r = MerkleHash([2u8; 32]);
    let h = merkle_node_hash(&l, &r).unwrap();
    assert_ne!(h.0, [0u8; 32]);
    // deterministic
    let h2 = merkle_node_hash(&l, &r).unwrap();
    assert_eq!(h, h2);
    // non-commutative
    let h3 = merkle_node_hash(&r, &l).unwrap();
    assert_ne!(h, h3);
}

// ─── Checkpoint ──────────────────────────────────────────────────────────────

#[test]
fn test_checkpoint_create_sign_verify() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let mut anchors = AnchorSet::new(1).unwrap();
    anchors.add(&bls_kp.pubkey(), true).unwrap();

    let root = MerkleHash([0x11u8; 32]);
    let mut cp = Checkpoint::new(1, now_ns(), root, 10).unwrap();
    assert_eq!(cp.epoch, 1);
    assert_eq!(cp.log_size, 10);
    cp.sign(&bls_kp).unwrap();
    cp.verify(&anchors).unwrap();
}

#[test]
fn test_checkpoint_encode_decode() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let root = MerkleHash([0x22u8; 32]);
    let mut cp = Checkpoint::new(5, now_ns(), root, 42).unwrap();
    cp.sign(&bls_kp).unwrap();

    let enc = cp.encode().unwrap();
    let dec = Checkpoint::decode(&enc).unwrap();
    assert_eq!(dec.epoch, 5);
    assert_eq!(dec.log_size, 42);
    assert_eq!(dec.merkle_root, root);
}

#[test]
fn test_checkpoint_message_non_empty() {
    init();
    let root = MerkleHash([0x33u8; 32]);
    let cp = Checkpoint::new(1, 1_000_000_000, root, 7).unwrap();
    let msg = cp.message().unwrap();
    assert!(!msg.is_empty());
}

// ─── AnchorSet ───────────────────────────────────────────────────────────────

#[test]
fn test_anchor_set_operations() {
    init();
    use nwep::bls::*;
    let kp1 = BlsKeypair::generate().unwrap();
    let kp2 = BlsKeypair::generate().unwrap();

    let mut anchors = AnchorSet::new(2).unwrap();
    assert_eq!(anchors.threshold(), 2);
    assert_eq!(anchors.size(), 0);

    anchors.add(&kp1.pubkey(), false).unwrap(); // non-builtin so we can remove it
    anchors.add(&kp2.pubkey(), false).unwrap();
    assert_eq!(anchors.size(), 2);
    assert!(anchors.contains(&kp1.pubkey()));
    assert!(anchors.contains(&kp2.pubkey()));
    // nwep_anchor_set_get is not implemented in this C library version (returns -803)
    // so we test containment and size, not indexed access

    anchors.remove(&kp1.pubkey()).unwrap();
    assert_eq!(anchors.size(), 1);
    assert!(!anchors.contains(&kp1.pubkey()));
}

#[test]
fn test_anchor_set_iter() {
    init();
    use nwep::bls::*;
    let mut anchors = AnchorSet::new(1).unwrap();
    for _ in 0..3 {
        let kp = BlsKeypair::generate().unwrap();
        anchors.add(&kp.pubkey(), false).unwrap();
    }
    let all: Vec<_> = anchors.iter().collect();
    assert_eq!(all.len(), 3);
}

// ─── IdentityCache ───────────────────────────────────────────────────────────

#[test]
fn test_identity_cache_store_lookup() {
    init();
    use nwep::cache::IdentityCacheSettings;
    let settings = IdentityCacheSettings { capacity: 100, ttl_ns: 3_600_000_000_000 };
    let mut cache = IdentityCache::new(Some(&settings)).unwrap();
    assert_eq!(cache.capacity(), 100);

    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let pk = kp.public_key();
    let now = now_ns();

    assert!(cache.lookup(&nid, now).is_err(), "miss before store");

    cache.store(&nid, &pk, 0, now).unwrap();
    let hit = cache.lookup(&nid, now).unwrap();
    assert_eq!(hit.node_id, nid);
    assert_eq!(hit.pubkey, pk);

    let stats = cache.stats();
    assert_eq!(stats.stores, 1);
    assert_eq!(stats.hits, 1);
    assert_eq!(stats.misses, 1);
}

#[test]
fn test_identity_cache_invalidate_and_clear() {
    init();
    let mut cache = IdentityCache::new(None).unwrap();
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let now = now_ns();

    cache.store(&nid, &kp.public_key(), 0, now).unwrap();
    cache.invalidate(&nid).unwrap();
    assert!(cache.lookup(&nid, now).is_err());

    // clear empties the cache
    cache.store(&nid, &kp.public_key(), 0, now).unwrap();
    assert_eq!(cache.size(), 1);
    cache.clear();
    assert_eq!(cache.size(), 0);

    cache.reset_stats();
    assert_eq!(cache.stats().hits, 0);
}

#[test]
fn test_identity_cache_on_rotation() {
    init();
    let mut cache = IdentityCache::new(None).unwrap();
    let kp1 = Keypair::generate().unwrap();
    let kp2 = Keypair::generate().unwrap();
    let nid = kp1.node_id().unwrap();
    let now = now_ns();

    cache.store(&nid, &kp1.public_key(), 0, now).unwrap();
    cache.on_rotation(&nid, &kp2.public_key(), 1, now).unwrap();
    let hit = cache.lookup(&nid, now).unwrap();
    assert_eq!(hit.pubkey, kp2.public_key());
    assert_eq!(hit.log_index, 1);
}

#[test]
fn test_identity_cache_on_revocation() {
    init();
    let mut cache = IdentityCache::new(None).unwrap();
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let now = now_ns();
    cache.store(&nid, &kp.public_key(), 0, now).unwrap();
    // Should not panic
    cache.on_revocation(&nid).unwrap();
}

// ─── LogServerPool ───────────────────────────────────────────────────────────

#[test]
fn test_log_server_pool_operations() {
    init();
    use nwep::{LogServerPool, ServerHealth};
    let mut pool = LogServerPool::new(None).unwrap();
    assert_eq!(pool.size(), 0);

    pool.add("web://server1.example.com/log").unwrap();
    pool.add("web://server2.example.com/log").unwrap();
    assert_eq!(pool.size(), 2);
    assert_eq!(pool.healthy_count(), 2);

    let s = pool.select().unwrap();
    assert!(!s.url.is_empty());
    assert!(matches!(s.health, ServerHealth::Healthy));

    let now = now_ns();
    pool.mark_success("web://server1.example.com/log", now);
    pool.mark_failure("web://server2.example.com/log", now);

    pool.remove("web://server1.example.com/log").unwrap();
    assert_eq!(pool.size(), 1);

    pool.reset_health();
}

#[test]
fn test_log_server_pool_round_robin() {
    init();
    use nwep::{LogServerPool, PoolSettings, PoolStrategy};
    let settings = PoolSettings { strategy: PoolStrategy::RoundRobin, max_failures: 3 };
    let mut pool = LogServerPool::new(Some(&settings)).unwrap();
    pool.add("web://a.example.com/log").unwrap();
    pool.add("web://b.example.com/log").unwrap();

    // Round-robin should alternate
    let s1 = pool.select().unwrap();
    let s2 = pool.select().unwrap();
    assert_ne!(s1.url, s2.url);
}

// ─── TrustStore ──────────────────────────────────────────────────────────────

#[test]
fn test_trust_store_basic() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let mut ts = TrustStore::new(TrustSettings::default()).unwrap();
    ts.add_anchor(&bls_kp.pubkey(), true).unwrap();
    // ts.anchors() is unsafe to call (unsound transmute in existing code), skip size check
    assert_eq!(ts.checkpoint_count(), 0);

    // No checkpoints → stale
    let s = ts.check_staleness(now_ns());
    assert!(matches!(s, Staleness::Warning | Staleness::Reject));
}

#[test]
fn test_trust_store_add_checkpoint() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let settings = TrustSettings { anchor_threshold: 1, ..TrustSettings::default() };
    let mut ts = TrustStore::new(settings).unwrap();
    ts.add_anchor(&bls_kp.pubkey(), true).unwrap();

    let root = MerkleHash([0xAAu8; 32]);
    let mut cp = Checkpoint::new(1, now_ns(), root, 5).unwrap();
    cp.sign(&bls_kp).unwrap();
    ts.add_checkpoint(&cp).unwrap();
    assert_eq!(ts.checkpoint_count(), 1);

    let latest = ts.get_latest_checkpoint().unwrap();
    assert_eq!(latest.epoch, 1);

    let by_epoch = ts.get_checkpoint(1).unwrap();
    assert_eq!(by_epoch.epoch, 1);

    // Fresh right after adding
    assert!(matches!(ts.check_staleness(now_ns()), Staleness::Fresh));
}

#[test]
fn test_trust_store_remove_anchor() {
    init();
    use nwep::bls::*;
    let kp = BlsKeypair::generate().unwrap();
    let mut ts = TrustStore::new(TrustSettings::default()).unwrap();
    ts.add_anchor(&kp.pubkey(), false).unwrap(); // non-builtin so we can remove it
    // ts.anchors() is unsafe to call (unsound transmute in existing code)
    ts.remove_anchor(&kp.pubkey()).unwrap();
    // verify remove succeeded: adding a duplicate should still work (size stays at 0 anchors)
}

#[test]
fn test_trust_store_staleness_warning() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    // Use a tiny warning threshold — checkpoint timestamp will be set 100ms in the past
    let settings = TrustSettings {
        staleness_warning_ns: 1_000_000,    // 1 ms warning threshold
        staleness_reject_ns: 3_600_000_000_000,
        identity_cache_ttl: nwep::trust::IDENTITY_CACHE_TTL,
        anchor_threshold: 1,
    };
    let mut ts = TrustStore::new(settings).unwrap();
    ts.add_anchor(&bls_kp.pubkey(), true).unwrap();

    // Use a timestamp 100ms in the past so the checkpoint is immediately stale
    let past_ts = now_ns().saturating_sub(100_000_000);
    let root = MerkleHash([0xBBu8; 32]);
    let mut cp = Checkpoint::new(1, past_ts, root, 1).unwrap();
    cp.sign(&bls_kp).unwrap();
    ts.add_checkpoint(&cp).unwrap();

    // 100ms old checkpoint with 1ms warning threshold → should be Warning or Reject
    let s = ts.check_staleness(now_ns());
    assert!(matches!(s, Staleness::Warning | Staleness::Reject));

    let age = ts.get_staleness_age(now_ns());
    assert!(age > 0);
}

#[test]
fn test_trust_store_cache_and_lookup_identity() {
    init();
    let mut ts = TrustStore::new(TrustSettings::default()).unwrap();
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    let now = now_ns();

    let vi = VerifiedIdentity {
        node_id: nid,
        pubkey: kp.public_key(),
        log_index: 3,
        checkpoint_epoch: 1,
        verified_at: now,
        revoked: false,
    };
    ts.cache_identity(&vi).unwrap();
    let got = ts.lookup_identity(&nid, now).unwrap();
    assert_eq!(got.node_id, nid);
    assert_eq!(got.pubkey, kp.public_key());
    assert_eq!(got.log_index, 3);
    assert!(!got.revoked);
}

#[test]
fn test_trust_store_with_storage_callbacks() {
    init();
    use std::sync::{Arc, Mutex};
    use nwep::bls::*;

    let saved_anchors: Arc<Mutex<Vec<BlsPubkey>>> = Arc::new(Mutex::new(vec![]));
    let saved_anchors_c = saved_anchors.clone();

    let saved_cps: Arc<Mutex<Vec<Checkpoint>>> = Arc::new(Mutex::new(vec![]));
    let saved_cps_c = saved_cps.clone();

    let storage = TrustStorage {
        anchor_load: None,
        anchor_save: Some(Box::new(move |pks: &[BlsPubkey]| {
            *saved_anchors_c.lock().unwrap() = pks.to_vec();
            Ok(())
        })),
        checkpoint_load: None,
        checkpoint_save: Some(Box::new(move |cp: &Checkpoint| {
            saved_cps_c.lock().unwrap().push(cp.clone());
            Ok(())
        })),
    };

    let settings = TrustSettings { anchor_threshold: 1, ..TrustSettings::default() };
    let mut ts = TrustStore::new_with_storage(settings, storage).unwrap();

    let bls_kp = BlsKeypair::generate().unwrap();
    ts.add_anchor(&bls_kp.pubkey(), true).unwrap();
    // Note: the C library does not call anchor_save on add_anchor (only on explicit persistence),
    // so we don't assert on saved_anchors here; we keep the callback wired up for coverage.

    let root = MerkleHash([0xCCu8; 32]);
    let mut cp = Checkpoint::new(1, now_ns(), root, 1).unwrap();
    cp.sign(&bls_kp).unwrap();
    ts.add_checkpoint(&cp).unwrap();
    // checkpoint_save should have been called on add_checkpoint
    assert_eq!(saved_cps.lock().unwrap().len(), 1);
}

#[test]
fn test_trust_store_check_equivocation() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let settings = TrustSettings { anchor_threshold: 1, ..TrustSettings::default() };
    let mut ts = TrustStore::new(settings).unwrap();
    ts.add_anchor(&bls_kp.pubkey(), true).unwrap();

    let now = now_ns();
    let mut cp1 = Checkpoint::new(1, now, MerkleHash([0x01u8; 32]), 1).unwrap();
    cp1.sign(&bls_kp).unwrap();
    ts.add_checkpoint(&cp1).unwrap();

    // Same epoch, different root = equivocation
    let mut cp2 = Checkpoint::new(1, now, MerkleHash([0x02u8; 32]), 1).unwrap();
    cp2.sign(&bls_kp).unwrap();
    let result = ts.check_equivocation(&cp2).unwrap();
    assert!(result.is_some(), "should detect equivocation for same epoch, different root");

    // Same epoch, same root = no equivocation
    let mut cp3 = Checkpoint::new(1, now, MerkleHash([0x01u8; 32]), 1).unwrap();
    cp3.sign(&bls_kp).unwrap();
    let result2 = ts.check_equivocation(&cp3).unwrap();
    assert!(result2.is_none(), "same root should not be equivocation");
}

// ─── ServerRole ──────────────────────────────────────────────────────────────

#[test]
fn test_server_role_from_str() {
    init();
    assert_eq!(ServerRole::from_str("regular"), ServerRole::Regular);
    assert_eq!(ServerRole::from_str("log_server"), ServerRole::LogServer);
    assert_eq!(ServerRole::from_str("anchor"), ServerRole::Anchor);
    assert_eq!(ServerRole::from_str("unknown"), ServerRole::Regular); // default
}

#[test]
fn test_server_role_as_str() {
    init();
    assert_eq!(ServerRole::Regular.as_str(), "regular");
    assert_eq!(ServerRole::LogServer.as_str(), "log_server");
    assert_eq!(ServerRole::Anchor.as_str(), "anchor");
    assert_eq!(format!("{}", ServerRole::LogServer), "log_server");
}

// ─── Logging ─────────────────────────────────────────────────────────────────

#[test]
fn test_log_levels_as_str() {
    init();
    use nwep::log::LogLevel::*;
    for lvl in [Trace, Debug, Info, Warn, Error] {
        assert!(!lvl.as_str().is_empty());
    }
}

#[test]
fn test_log_callback_receives_entry() {
    init();
    use std::sync::{Arc, Mutex};
    use nwep::log::*;

    let received: Arc<Mutex<Vec<LogEntry>>> = Arc::new(Mutex::new(vec![]));
    let rc = received.clone();
    set_callback(move |e| rc.lock().unwrap().push(e));
    set_level(LogLevel::Info);

    info(&[0u8; 16], "test_comp", "hello from test");
    std::thread::sleep(std::time::Duration::from_millis(20));

    let got = received.lock().unwrap();
    assert!(!got.is_empty(), "callback should have received an entry");
    assert_eq!(got[0].message, "hello from test");
    assert_eq!(got[0].component, "test_comp");

    drop(got);
    clear_callback();
}

#[test]
fn test_log_format_json() {
    init();
    use nwep::log::*;
    let entry = LogEntry {
        level: LogLevel::Warn,
        timestamp_ns: 9_876_543_210,
        trace_id: [0u8; 16],
        component: "unit_test".to_string(),
        message: "format test".to_string(),
    };
    let json = entry.format_json();
    assert!(!json.is_empty());
    assert!(json.contains("format test"));
}

// ─── LogIndex ────────────────────────────────────────────────────────────────

#[test]
fn test_log_index_update_and_lookup() {
    init();
    let mut idx = LogIndex::new(MemIndex(HashMap::new())).unwrap();
    let kp = Keypair::generate().unwrap();
    let entry = make_entry(&kp, now_ns());

    idx.update(&entry, 42).unwrap();

    let got = idx.lookup(&entry.node_id).unwrap();
    assert_eq!(got.node_id, entry.node_id);
    assert_eq!(got.pubkey, entry.pubkey);
    assert_eq!(got.log_index, 42);
    assert!(!got.revoked);
}

#[test]
fn test_log_index_lookup_missing() {
    init();
    let mut idx = LogIndex::new(MemIndex(HashMap::new())).unwrap();
    let kp = Keypair::generate().unwrap();
    let nid = kp.node_id().unwrap();
    assert!(idx.lookup(&nid).is_err(), "lookup of absent entry should fail");
}

// ─── AnchorServer (standalone) ───────────────────────────────────────────────

#[test]
fn test_anchor_server_create_and_sign_proposal() {
    init();
    use nwep::bls::*;
    let bls_kp = BlsKeypair::generate().unwrap();
    let mut anchors = AnchorSet::new(1).unwrap();
    anchors.add(&bls_kp.pubkey(), true).unwrap();

    let mut log = MerkleLog::new(MemLog(vec![])).unwrap();
    let mut as_ = AnchorServer::new(&bls_kp, &mut anchors, AnchorServerSettings::default()).unwrap();

    let mut cp = as_.create_proposal(&mut log, 1, now_ns()).unwrap();
    assert_eq!(cp.epoch, 1);

    as_.sign_proposal(&mut cp).unwrap();
    assert!(!cp.signers.is_empty());

    as_.add_checkpoint(&cp).unwrap();
    let latest = as_.get_latest().unwrap();
    assert_eq!(latest.epoch, 1);

    let by_epoch = as_.get_checkpoint(1).unwrap();
    assert_eq!(by_epoch.epoch, 1);
}

// ─── Protocol uint helpers ───────────────────────────────────────────────────

#[test]
fn test_put_get_uint32be() {
    init();
    let mut buf = [0u8; 4];
    nwep::protocol::put_uint32be(&mut buf, 0xDEADBEEF);
    let v = nwep::protocol::get_uint32be(&buf);
    assert_eq!(v, 0xDEADBEEF);
}

#[test]
fn test_put_get_uint16be() {
    init();
    let mut buf = [0u8; 2];
    nwep::protocol::put_uint16be(&mut buf, 0x1234);
    let v = nwep::protocol::get_uint16be(&buf);
    assert_eq!(v, 0x1234);
}

