//! CR-7 conformance runner — Rust binding.
//!
//! Re-asserts the named scenarios in `scenario.json`. Value-level byte equality is
//! not achievable here (OpenMLS internals are non-deterministic); we lock down the
//! invariants instead. See the integration test `cr7_group_state_snapshot.rs` for
//! the in-depth assertions.

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

use std::path::PathBuf;
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, GroupSnapshotEntry, GroupStateSnapshot, Identity,
    MessageEnvelope, MessagingClient, Storage, StorageBackend, SyncCursor, Transport,
    GROUP_SNAPSHOT_HARD_CAP, GROUP_SNAPSHOT_VERSION,
};

fn fixture_path() -> PathBuf {
    let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    p.push("../../protocol/conformance/cr7_group_state_snapshot/scenario.json");
    p
}

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

#[test]
fn cr7_conformance_scenarios() {
    let json =
        std::fs::read_to_string(fixture_path()).unwrap_or_else(|e| panic!("read fixture: {e}"));
    for required in [
        "round_trip_identity",
        "cross_device_peer_decrypt",
        "caps_enforced",
        "import_rejects_mismatched_openmls_version",
        "import_rejects_already_open_conversation",
    ] {
        assert!(
            json.contains(required),
            "fixture missing scenario {required}"
        );
    }

    // --- round_trip_identity ---
    {
        let (alice, _) = open_client("A", Identity::generate());
        let (bob, _) = open_client("B", Identity::generate());
        let kp = bob.fresh_key_package().unwrap();
        let conv = block_on(alice.create_conversation(None, 1_000)).unwrap();
        block_on(alice.add_members(conv, vec![(bob.device_id(), kp)], 2_000)).unwrap();

        let bytes = alice
            .export_conversation_state_snapshot(conv, 3_000)
            .unwrap();
        assert!(!bytes.is_empty());

        let decoded = GroupStateSnapshot::decode(&bytes).unwrap();
        assert_eq!(decoded.v, GROUP_SNAPSHOT_VERSION);
        assert_eq!(decoded.group_id, conv);
        assert_eq!(
            decoded.openmls_storage_version,
            openmls_traits::storage::CURRENT_VERSION
        );
        assert!(!decoded.entries.is_empty());

        let re_encoded = decoded.encode().unwrap();
        assert_eq!(
            *bytes, re_encoded,
            "encode→decode→encode must be byte-identical"
        );
    }

    // --- cross_device_peer_decrypt ---
    // Same logic as cr7_group_state_snapshot::cross_device_import_decrypts_subsequent_traffic
    // (kept here for cross-binding-conformance parity — when JS/Swift/Kotlin runners
    // ship, they replay this same flow).
    {
        let alice_identity = Identity::generate();
        let alice_export = alice_identity.export();
        let (alice1, t_a) = open_client("A1", alice_identity);
        let (peer, _) = open_client("P", Identity::generate());

        let p_kp = peer.fresh_key_package().unwrap();
        let conv = block_on(alice1.create_conversation(None, 1_000)).unwrap();
        block_on(alice1.add_members(conv, vec![(peer.device_id(), p_kp)], 2_000)).unwrap();
        let welcome = t_a
            .sent
            .lock()
            .iter()
            .rev()
            .find(|e| matches!(e.kind, ping_core::message::MessageKind::Welcome))
            .cloned()
            .expect("welcome");
        block_on(peer.join_conversation(&welcome, 3_000)).unwrap();
        let _ = block_on(alice1.send(conv, b"warm".to_vec(), 4_000)).unwrap();

        let (alice2, _) = open_client("A2", Identity::import(&alice_export).unwrap());
        let snap = alice1
            .export_conversation_state_snapshot(conv, 5_000)
            .unwrap();
        let imported = block_on(alice2.import_state_snapshot(&snap, 6_000)).unwrap();
        assert_eq!(imported, conv);

        let env_from_p = block_on(peer.send(conv, b"peer-post-link".to_vec(), 7_000)).unwrap();
        let msg = block_on(alice2.process_envelope(&env_from_p, 8_000))
            .unwrap()
            .expect("app msg");
        assert_eq!(msg.plaintext, b"peer-post-link");
    }

    // --- caps_enforced ---
    {
        let bloat = vec![0xab; GROUP_SNAPSHOT_HARD_CAP + 1];
        let snap = GroupStateSnapshot {
            v: GROUP_SNAPSHOT_VERSION,
            group_id: ConversationId([0u8; 16]),
            openmls_storage_version: openmls_traits::storage::CURRENT_VERSION,
            snapshot_created_at_ms: 0,
            entries: vec![GroupSnapshotEntry {
                key: b"k".to_vec(),
                value: bloat,
            }],
        };
        assert!(snap.encode().is_err());
        let oversize = vec![0u8; GROUP_SNAPSHOT_HARD_CAP + 100];
        assert!(GroupStateSnapshot::decode(&oversize).is_err());

        let bogus = GroupStateSnapshot {
            v: 99,
            group_id: ConversationId([0u8; 16]),
            openmls_storage_version: openmls_traits::storage::CURRENT_VERSION,
            snapshot_created_at_ms: 0,
            entries: Vec::new(),
        };
        let bytes = bogus.encode().unwrap();
        assert!(GroupStateSnapshot::decode(&bytes).is_err());
    }

    // --- import_rejects_mismatched_openmls_version ---
    {
        let snap = GroupStateSnapshot {
            v: GROUP_SNAPSHOT_VERSION,
            group_id: ConversationId([7u8; 16]),
            openmls_storage_version: 999,
            snapshot_created_at_ms: 0,
            entries: vec![GroupSnapshotEntry {
                key: b"p".to_vec(),
                value: b"p".to_vec(),
            }],
        };
        let bytes = snap.encode().unwrap();
        let (alice, _) = open_client("A", Identity::generate());
        let err = block_on(alice.import_state_snapshot(&bytes, 1_000)).unwrap_err();
        assert!(format!("{err}").contains("openmls_storage_version"));
    }

    // --- import_rejects_already_open_conversation ---
    {
        let (alice, _) = open_client("A", Identity::generate());
        let conv = block_on(alice.create_conversation(None, 1_000)).unwrap();
        let snap = alice
            .export_conversation_state_snapshot(conv, 2_000)
            .unwrap();
        let err = block_on(alice.import_state_snapshot(&snap, 3_000)).unwrap_err();
        assert!(format!("{err}").contains("already open"));
    }
}
