//! CR-10 — `MessagingClient::init` ensures the user's DeviceGroup exists.
//!
//! Single-device users need somewhere to write personal events (drafts, read pointers,
//! notes, vault wrapper) the instant they sign in. The previous behaviour created the
//! DG lazily inside `build_linking_ticket`, so a never-pairs-a-second-device user had
//! NO DeviceGroup and personal-state writes had no target.
//!
//! Conformance:
//! 1. After `MessagingClient::init`, `list_conversations()` contains exactly one entry
//!    with `is_device_group == true`.
//! 2. The DG id is deterministic — `device_group_id_for(user_id)` — so calling init a
//!    second time (cold restart) returns the same id.
//! 3. The DG survives a cold restart: persisted by `snapshot_to_storage` during init,
//!    rehydrated on re-init, NOT re-created (so member_count stays at whatever it was).

#![cfg(feature = "test-utils")]

use std::sync::Arc;

use futures::executor::block_on;
use parking_lot::Mutex;

use ping_core::{
    storage::memory::MemStorage, transport::DiscoveredDevice, transport::TransportSubscription,
    ClientConfig, ConversationId, Identity, MessageEnvelope, MessagingClient, Storage, SyncCursor,
    Transport,
};

#[derive(Debug, Default)]
struct NoopTransport {
    sent: Mutex<Vec<MessageEnvelope>>,
    revoked: Mutex<Vec<ping_core::DeviceId>>,
}

impl Transport for NoopTransport {
    fn send<'a>(
        &'a self,
        envelope: MessageEnvelope,
    ) -> ping_core::transport::TransportFuture<'a, ()> {
        Box::pin(async move {
            self.sent.lock().push(envelope);
            Ok(())
        })
    }
    fn fetch_since<'a>(
        &'a self,
        _conv: ConversationId,
        _cursor: SyncCursor,
        _limit: u32,
    ) -> ping_core::transport::TransportFuture<'a, Vec<MessageEnvelope>> {
        Box::pin(async move { Ok(Vec::new()) })
    }
    fn subscribe<'a>(
        &'a self,
        _cb: Arc<dyn Fn(MessageEnvelope) + Send + Sync>,
    ) -> ping_core::transport::TransportFuture<'a, TransportSubscription> {
        Box::pin(async move { Ok(TransportSubscription::new(|| ())) })
    }
    fn discover_devices<'a>(
        &'a self,
        _user_id: ping_core::UserId,
    ) -> ping_core::transport::TransportFuture<'a, Vec<DiscoveredDevice>> {
        Box::pin(async move { Ok(Vec::new()) })
    }
    fn revoke_device_remote<'a>(
        &'a self,
        device_id: ping_core::DeviceId,
    ) -> ping_core::transport::TransportFuture<'a, ()> {
        Box::pin(async move {
            self.revoked.lock().push(device_id);
            Ok(())
        })
    }
}

fn fresh_client_with_shared_storage(
    transport: Arc<NoopTransport>,
    identity: Identity,
    storage: Arc<dyn Storage>,
) -> Arc<MessagingClient> {
    let cfg = ClientConfig {
        identity,
        device_label: "tester".into(),
        storage,
        transport,
        now_ms: 1_000,
        storage_backend: ping_core::StorageBackend::Memory,
        device_signing_secret_key: None,
    };
    block_on(MessagingClient::init(cfg)).expect("init")
}

#[test]
fn init_creates_device_group_eagerly() {
    let transport = Arc::new(NoopTransport::default());
    let storage: Arc<dyn Storage> = Arc::new(MemStorage::default());
    let identity = Identity::generate();
    let client = fresh_client_with_shared_storage(transport, identity, storage);

    let convos = client.list_conversations();
    let dg_count = convos.iter().filter(|c| c.is_device_group).count();
    assert_eq!(
        dg_count, 1,
        "init should create exactly one DeviceGroup (got {dg_count})"
    );
}

#[test]
fn revoke_device_calls_transport_revoke_remote() {
    // Audit gap closed: client-initiated revoke must propagate to the auth
    // layer via Transport::revoke_device_remote. The MLS commits land first;
    // the server notification is the eventual-consistency cleanup.
    let transport_a = Arc::new(NoopTransport::default());
    let transport_b = Arc::new(NoopTransport::default());
    let client_a = fresh_client_with_shared_storage(
        transport_a.clone(),
        Identity::generate(),
        Arc::new(MemStorage::default()),
    );
    let client_b = fresh_client_with_shared_storage(
        transport_b.clone(),
        Identity::generate(),
        Arc::new(MemStorage::default()),
    );

    let b_kp = client_b.fresh_key_package().expect("kp");
    let conv = block_on(client_a.create_conversation(None, 1_000)).expect("create");
    block_on(client_a.add_members(conv, vec![(client_b.device_id(), b_kp)], 2_000)).expect("add");

    let _envelopes = block_on(client_a.revoke_device(client_b.device_id(), 3_000)).expect("revoke");

    let revoked = transport_a.revoked.lock();
    assert_eq!(
        revoked.len(),
        1,
        "expected exactly one revoke_device_remote call"
    );
    assert_eq!(revoked[0], client_b.device_id());
}

#[test]
fn device_group_survives_cold_restart() {
    // Init the client, then re-init with the same storage + same identity
    // (round-tripped via export/import so we get a stable user_id and
    // signing key). The second init must NOT create a second DG —
    // rehydrate finds the existing one first and ensure_device_group must
    // be idempotent against that.
    let transport = Arc::new(NoopTransport::default());
    let storage: Arc<dyn Storage> = Arc::new(MemStorage::default());
    let identity_a = Identity::generate();
    let identity_export = identity_a.export();

    let first = fresh_client_with_shared_storage(transport.clone(), identity_a, storage.clone());
    let first_dg_id = first
        .list_conversations()
        .into_iter()
        .find(|c| c.is_device_group)
        .map(|c| c.id)
        .expect("first init produced a DG");
    drop(first);

    let identity_b = Identity::import(&identity_export).expect("identity round-trip");
    let second = fresh_client_with_shared_storage(transport, identity_b, storage);
    let dgs: Vec<_> = second
        .list_conversations()
        .into_iter()
        .filter(|c| c.is_device_group)
        .collect();
    assert_eq!(dgs.len(), 1, "cold restart must yield exactly one DG");
    assert_eq!(
        dgs[0].id, first_dg_id,
        "DG id must be deterministic across restarts"
    );
}
