//! Leave-group via a self-Remove proposal committed by a remaining member.
//!
//! MLS forbids committing your own removal, so `leave_conversation` broadcasts a
//! Remove PROPOSAL; a remaining member processes it (OpenMLS buffers it) and
//! commits it via `commit_pending_proposals`, which evicts the leaver.

#![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, MessageKind, MessagingClient, Storage,
    SyncCursor, Transport,
};

#[derive(Debug)]
struct CaptureTransport {
    sent: Mutex<Vec<MessageEnvelope>>,
}
impl CaptureTransport {
    fn new() -> Self {
        Self {
            sent: Mutex::new(Vec::new()),
        }
    }
}
impl Transport for CaptureTransport {
    fn send<'a>(&'a self, e: MessageEnvelope) -> ping_core::transport::TransportFuture<'a, ()> {
        Box::pin(async move {
            self.sent.lock().push(e);
            Ok(())
        })
    }
    fn fetch_since<'a>(
        &'a self,
        _c: ConversationId,
        _cur: SyncCursor,
        _l: 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,
        _u: ping_core::UserId,
    ) -> ping_core::transport::TransportFuture<'a, Vec<DiscoveredDevice>> {
        Box::pin(async move { Ok(Vec::new()) })
    }
}
fn make(label: &str, t: Arc<CaptureTransport>) -> Arc<MessagingClient> {
    block_on(MessagingClient::init(ClientConfig {
        identity: Identity::generate(),
        device_label: label.into(),
        storage: Arc::new(MemStorage::default()) as Arc<dyn Storage>,
        transport: t,
        now_ms: 1_000,
        storage_backend: ping_core::StorageBackend::Memory,
        device_signing_secret_key: None,
    }))
    .expect("init")
}

#[test]
fn leave_proposal_committed_by_remaining_member_evicts_leaver() {
    let ta = Arc::new(CaptureTransport::new());
    let tb = Arc::new(CaptureTransport::new());
    let a = make("A", ta.clone());
    let b = make("B", tb.clone());

    // A creates a group and adds B.
    let conv = block_on(a.create_conversation(Some("g".into()), 1_000)).expect("create");
    let b_kp = block_on(b.fresh_key_package()).expect("kp");
    block_on(a.add_members(conv, vec![(b.device_id(), b_kp)], 2_000)).expect("add B");
    let welcome = ta
        .sent
        .lock()
        .iter()
        .find(|e| e.kind == MessageKind::Welcome)
        .cloned()
        .expect("welcome");
    block_on(b.join_conversation(&welcome, 3_000)).expect("B join");

    // B leaves: broadcasts a self-Remove proposal.
    block_on(b.leave_conversation(conv, 4_000)).expect("B leave");
    let proposal = tb
        .sent
        .lock()
        .iter()
        .rev()
        .find(|e| e.kind == MessageKind::Proposal)
        .cloned()
        .expect("leave proposal captured");

    // A processes B's leave proposal (buffered) and now has a pending proposal.
    block_on(a.process_envelope(&proposal, 5_000)).expect("A process proposal");
    assert_eq!(
        a.conversations_with_pending_proposals(),
        vec![conv],
        "A should see a pending proposal to commit"
    );

    // A (a remaining member) commits the pending proposal → evicts B.
    block_on(a.commit_pending_proposals(conv, 6_000)).expect("A commits leave");
    let b_user = b.user_id();
    let a_sees_b = a.members(conv).into_iter().any(|m| m.user_id == b_user);
    assert!(
        !a_sees_b,
        "A must no longer see B after committing B's leave"
    );
    assert!(
        a.conversations_with_pending_proposals().is_empty(),
        "pending proposals should clear after the commit"
    );
}
