//! CR-6 — `content_hash` over application plaintext.
//!
//! Two invariants we care about:
//!   1. **Rebase parity**: sending the same plaintext at two different epochs produces two
//!      envelopes whose `content_hash` is identical. (The whole point — what makes
//!      `event_id`-survives-rebase work.)
//!   2. **Post-decrypt verify**: a v=2 application envelope whose `content_hash` was
//!      tampered with must be rejected at `process_envelope` time, after MLS decrypt
//!      revealed the plaintext that no longer matches the hash.

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

use std::sync::Arc;

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

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

#[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("init")
}

#[test]
fn application_content_hash_is_plaintext_sha256() {
    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 plaintext = b"hello, plaintext-hash world";
    let env = block_on(client.send(conv, plaintext.to_vec(), 2_000)).expect("send");

    let expected = hash_application_plaintext(plaintext);
    assert_eq!(env.v, 2, "WIRE_VERSION should be 2 post-CR-6");
    assert_eq!(
        env.content_hash, expected,
        "content_hash must hash plaintext"
    );
}

#[test]
fn same_plaintext_at_different_epochs_produces_same_content_hash() {
    // Pure CR-6 rebase invariant. A sends "hello" at the initial epoch, then we mutate
    // the group state (add a member → epoch bumps), then A sends "hello" again. The two
    // envelopes' content_hashes MUST be identical. Under v=1's `SHA-256(kind || ciphertext)`
    // they would differ because the ciphertext is freshly randomized.
    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());

    let conv = block_on(client_a.create_conversation(None, 1_000)).expect("create");

    let pt = b"rebase-me".to_vec();
    let env_v0 = block_on(client_a.send(conv, pt.clone(), 2_000)).expect("send v0");
    let epoch_before = env_v0.epoch;

    // Bump the epoch.
    let b_kp = client_b.fresh_key_package().expect("kp");
    block_on(client_a.add_members(conv, vec![(client_b.device_id(), b_kp)], 3_000)).expect("add B");

    let env_v1 = block_on(client_a.send(conv, pt.clone(), 4_000)).expect("send v1");
    assert!(
        env_v1.epoch > epoch_before,
        "epoch must have advanced after add_members"
    );

    assert_eq!(
        env_v0.content_hash, env_v1.content_hash,
        "CR-6 rebase invariant: same plaintext → same content_hash across epochs"
    );

    // Sanity: different plaintext at the same epoch → different hash.
    let env_v2 = block_on(client_a.send(conv, b"different".to_vec(), 5_000)).expect("send v2");
    assert_ne!(env_v1.content_hash, env_v2.content_hash);
}

#[test]
fn tampered_application_content_hash_rejected_on_decrypt() {
    // Sender A and receiver B. A sends an envelope; we mutate `content_hash` on the wire;
    // B's `process_envelope` must reject because the post-decrypt hash check fails.
    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());

    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 B");

    // B joins via the Welcome A's transport captured.
    let welcome = transport_a
        .sent
        .lock()
        .iter()
        .find(|e| matches!(e.kind, ping_core::message::MessageKind::Welcome))
        .cloned()
        .expect("welcome");
    block_on(client_b.join_conversation(&welcome, 3_000)).expect("B join");

    let env = block_on(client_a.send(conv, b"genuine".to_vec(), 4_000)).expect("send");
    assert_eq!(env.v, 2);

    // Tamper: flip the first byte of content_hash. The payload (ciphertext) is unchanged,
    // so MLS decrypt still works, revealing the original plaintext — which no longer
    // matches the hash. CR-6 path rejects.
    let mut tampered = env.clone();
    tampered.content_hash[0] ^= 0xff;

    let err = block_on(client_b.process_envelope(&tampered, 5_000)).unwrap_err();
    let msg = format!("{err}");
    assert!(
        msg.contains("content_hash"),
        "expected hash-mismatch error, got: {msg}"
    );
}
