//! CR-8 — `MessagingClient::export_conversation_secret` integration tests.
//!
//! Two-member group: A creates a conversation, adds B via KeyPackage. Both clients run
//! `export_conversation_secret` with identical `(label, context, length)` and the bytes
//! must match — that's the whole reason CR-8 exists (it's what keys the ephemeral channel
//! and call media).
//!
//! Gated on `feature = "test-utils"` because we depend on the in-memory storage. CI runs
//! `cargo test --workspace --features test-utils`.

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

// A trivial transport that captures every `send` into a shared queue keyed by
// `(conversation_id, kind)`. Tests pull envelopes out manually and hand them to the
// other client's `process_envelope` / `join_conversation`.
//
// Deliberately not full-featured (no subscribe fan-out, no ordering guarantees beyond
// FIFO per-conv) — just enough to let two clients exchange a Welcome + Commit and then
// sit on the resulting MLS state for an export_secret check.
#[derive(Debug, Default)]
struct CapturingTransport {
    sent: Mutex<Vec<MessageEnvelope>>,
}

impl Transport for CapturingTransport {
    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: ping_core::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 make_client(label: &str, transport: Arc<CapturingTransport>) -> Arc<MessagingClient> {
    let cfg = ClientConfig {
        identity: Identity::generate(),
        device_label: label.into(),
        storage: Arc::new(MemStorage::default()) as Arc<dyn Storage>,
        transport,
        now_ms: 1_000,
        storage_backend: ping_core::StorageBackend::Memory,
        device_signing_secret_key: None,
    };
    block_on(MessagingClient::init(cfg)).expect("client init")
}

#[test]
fn export_secret_matches_across_members() {
    let transport_a = Arc::new(CapturingTransport::default());
    let transport_b = Arc::new(CapturingTransport::default());
    let client_a = make_client("A", transport_a.clone());
    let client_b = make_client("B", transport_b.clone());

    // B publishes a KeyPackage that A will use to invite it.
    let b_kp = client_b.fresh_key_package().expect("b keypackage");

    // A creates the conversation, then adds B.
    let conv_id = block_on(client_a.create_conversation(Some("test".into()), 2_000))
        .expect("create conversation");
    block_on(client_a.add_members(conv_id, vec![(client_b.device_id(), b_kp)], 3_000))
        .expect("add B");

    // Pull the Welcome out of A's transport queue and hand it to B.
    let welcome = transport_a
        .sent
        .lock()
        .iter()
        .find(|e| e.kind == ping_core::message::MessageKind::Welcome)
        .cloned()
        .expect("welcome captured by A's transport");
    block_on(client_b.join_conversation(&welcome, 4_000)).expect("B join");

    // Same `(conv, label, context, length)` MUST produce identical bytes on both sides.
    let label = "ping/test/cr8/v1";
    let context = b"fixture-context-bytes";
    let length = 32;

    let secret_a = client_a
        .export_conversation_secret(conv_id, label, context, length)
        .expect("A export");
    let secret_b = client_b
        .export_conversation_secret(conv_id, label, context, length)
        .expect("B export");

    assert_eq!(secret_a.len(), length, "A length mismatch");
    assert_eq!(secret_b.len(), length, "B length mismatch");
    assert_eq!(
        secret_a.as_slice(),
        secret_b.as_slice(),
        "exported secrets must match across members at the same epoch",
    );

    // Sanity: different label must produce different bytes (else the label is being ignored).
    let other = client_a
        .export_conversation_secret(conv_id, "ping/test/cr8/v2", context, length)
        .expect("A export other label");
    assert_ne!(
        secret_a.as_slice(),
        other.as_slice(),
        "different labels must derive different secrets",
    );

    // Sanity: different context must also differ.
    let other_ctx = client_a
        .export_conversation_secret(conv_id, label, b"other-context", length)
        .expect("A export other context");
    assert_ne!(
        secret_a.as_slice(),
        other_ctx.as_slice(),
        "different contexts must derive different secrets",
    );
}

#[test]
fn export_secret_validates_length() {
    let transport = Arc::new(CapturingTransport::default());
    let client = make_client("solo", transport);
    let conv = block_on(client.create_conversation(None, 1_000)).expect("create");

    let too_short = client.export_conversation_secret(conv, "ping/test", &[], 0);
    assert!(too_short.is_err(), "length 0 must be rejected");

    let too_long = client.export_conversation_secret(conv, "ping/test", &[], 1025);
    assert!(too_long.is_err(), "length > 1024 must be rejected");

    let ok = client.export_conversation_secret(conv, "ping/test", &[], 32);
    assert!(ok.is_ok(), "length 32 must succeed");
}

#[test]
fn export_secret_unknown_conversation() {
    let transport = Arc::new(CapturingTransport::default());
    let client = make_client("solo", transport);
    let bogus = ping_core::ConversationId([0u8; 16]);
    let err = client.export_conversation_secret(bogus, "ping/test", &[], 32);
    assert!(err.is_err(), "unknown conversation must error");
}
