//! CR-4 — persistent OpenMLS provider.
//!
//! What this proves: after a `MessagingClient` is dropped, a fresh one re-opened
//! against the same on-disk SQLite file can decrypt new MLS traffic without needing
//! to be re-Welcomed. That is exactly what iOS NSE (Notification Service Extension)
//! requires — NSE spins up a brand-new process to decrypt a push payload; the prior
//! app process is gone.
//!
//! Two clients (A and B). A creates a conversation and admits B. B joins, drops,
//! re-opens with `StorageBackend::Sqlite`, and decrypts a fresh message A sent in
//! the meantime. Without CR-4 this scenario fails — `OpenMlsRustCrypto`'s in-memory
//! keystore is empty in the new process.

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

use std::sync::Arc;

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

use ping_core::{
    storage::memory::MemStorage, transport::DiscoveredDevice, transport::TransportSubscription,
    ClientConfig, ConversationId, Identity, MessageEnvelope, MessagingClient, Storage,
    StorageBackend, 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: 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,
    transport: Arc<CapturingTransport>,
    host_storage: Arc<dyn Storage>,
    identity: Identity,
    backend: StorageBackend,
) -> Arc<MessagingClient> {
    let cfg = ClientConfig {
        identity,
        device_label: label.into(),
        storage: host_storage,
        transport,
        now_ms: 1_000,
        storage_backend: backend,
        device_signing_secret_key: None,
    };
    block_on(MessagingClient::init(cfg)).expect("init")
}

#[test]
fn sqlite_backend_persists_state_across_a_cold_restart() {
    // iOS NSE simulation. Workflow:
    //   1. A and B both initialised against their own SQLite files.
    //   2. A admits B → B joins.
    //   3. A sends a message; B decrypts.
    //   4. B's client is dropped (simulates app suspension).
    //   5. B is re-opened against the SAME SQLite file (simulates NSE process).
    //   6. A sends a *new* message; B decrypts. Crucially: B did not get re-Welcomed.
    let tmp = TempDir::new().expect("tempdir");
    let a_path = tmp.path().join("a.sqlite");
    let b_path = tmp.path().join("b.sqlite");

    let transport_a = Arc::new(CapturingTransport::default());
    let transport_b = Arc::new(CapturingTransport::default());

    // Host-side identities + KV stay constant across "process restarts" — that's how
    // real hosts behave (Keychain-backed identity export, durable app-storage). We
    // reuse the same `Identity` bytes and the same in-memory KV across both opens
    // of client B.
    let a_identity = Identity::generate();
    let b_identity_export = Identity::generate().export();
    let a_host_storage = Arc::new(MemStorage::default()) as Arc<dyn Storage>;
    let b_host_storage = Arc::new(MemStorage::default()) as Arc<dyn Storage>;

    let client_a = open_client(
        "A",
        transport_a.clone(),
        a_host_storage,
        a_identity,
        StorageBackend::Sqlite {
            path: a_path.clone(),
            encryption_key: None,
        },
    );
    let client_b = open_client(
        "B",
        transport_b.clone(),
        b_host_storage.clone(),
        Identity::import(&b_identity_export).expect("identity import"),
        StorageBackend::Sqlite {
            path: b_path.clone(),
            encryption_key: None,
        },
    );

    // A creates the conversation and admits B.
    let conv = block_on(client_a.create_conversation(Some("cr4".into()), 1_000)).expect("create");
    let kp = client_b.fresh_key_package().expect("kp");
    block_on(client_a.add_members(conv, vec![(client_b.device_id(), kp)], 2_000)).expect("add B");

    // B joins via the Welcome A captured.
    let welcome = transport_a
        .sent
        .lock()
        .iter()
        .rev()
        .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");

    // First exchange — A sends "before restart"; B decrypts. This warms B's state
    // and forces a checkpoint to the on-disk SQLite file.
    let env_before =
        block_on(client_a.send(conv, b"before-restart".to_vec(), 4_000)).expect("A send before");
    let msg_before = block_on(client_b.process_envelope(&env_before, 5_000))
        .expect("B process before")
        .expect("application msg");
    assert_eq!(msg_before.plaintext, b"before-restart");

    // Cold restart: drop client_b. Verify the SQLite file got written.
    drop(client_b);
    let len_before = std::fs::metadata(&b_path).expect("b.sqlite exists").len();
    assert!(
        len_before > 0,
        "SQLite file should be populated after checkpoint"
    );

    // Re-open B against the same path + same host KV + same identity bytes. This is
    // what a fresh NSE process does: read identity from Keychain, point at the same
    // on-disk MLS file, and continue.
    let client_b2 = open_client(
        "B-rev",
        transport_b.clone(),
        b_host_storage.clone(),
        Identity::import(&b_identity_export).expect("identity import 2"),
        StorageBackend::Sqlite {
            path: b_path.clone(),
            encryption_key: None,
        },
    );

    // A sends a new message — B was offline for this; the only way B can decrypt is
    // if its MLS state was loaded from disk on re-init.
    let env_after =
        block_on(client_a.send(conv, b"after-restart".to_vec(), 6_000)).expect("A send after");
    let msg_after = block_on(client_b2.process_envelope(&env_after, 7_000))
        .expect("B2 process after — THIS is the iOS NSE invariant")
        .expect("application msg");
    assert_eq!(
        msg_after.plaintext, b"after-restart",
        "decryption after cold restart MUST succeed — that's what CR-4 buys us"
    );
}

#[test]
fn memory_backend_does_not_persist() {
    // Same workflow as above, but with `StorageBackend::Memory` for B. After restart,
    // decryption MUST fail — proving that the cold-restart invariant is provided by
    // SQLite, not by the host-side `Storage` (which only persists meta + cursor).
    let transport_a = Arc::new(CapturingTransport::default());
    let transport_b = Arc::new(CapturingTransport::default());

    let a_identity = Identity::generate();
    let b_identity_export = Identity::generate().export();
    let a_host = Arc::new(MemStorage::default()) as Arc<dyn Storage>;
    let b_host = Arc::new(MemStorage::default()) as Arc<dyn Storage>;

    let client_a = open_client(
        "A",
        transport_a.clone(),
        a_host,
        a_identity,
        StorageBackend::Memory,
    );
    let client_b = open_client(
        "B",
        transport_b.clone(),
        b_host.clone(),
        Identity::import(&b_identity_export).expect("identity"),
        StorageBackend::Memory,
    );

    let conv = block_on(client_a.create_conversation(None, 1_000)).expect("create");
    let kp = client_b.fresh_key_package().expect("kp");
    block_on(client_a.add_members(conv, vec![(client_b.device_id(), kp)], 2_000)).unwrap();
    let welcome = transport_a
        .sent
        .lock()
        .iter()
        .rev()
        .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"warm".to_vec(), 4_000)).expect("A send warm");
    block_on(client_b.process_envelope(&env, 5_000)).expect("warm decrypt OK");

    // Restart B against in-memory.
    drop(client_b);
    let client_b2 = open_client(
        "B-rev",
        transport_b.clone(),
        b_host.clone(),
        Identity::import(&b_identity_export).expect("identity 2"),
        StorageBackend::Memory,
    );

    // The conv isn't even in B2's open set — B2 doesn't know it exists. Sending a
    // process_envelope for it should fail.
    let env_after = block_on(client_a.send(conv, b"cold".to_vec(), 6_000)).expect("A send cold");
    let result = block_on(client_b2.process_envelope(&env_after, 7_000));
    assert!(
        result.is_err(),
        "Memory backend must NOT survive restart — this proves SQLite is the difference"
    );
}

#[test]
fn sender_state_survives_cold_restart_between_sends() {
    // Regression: A sends a message, then cold-restarts, then sends again. B must
    // decrypt the second message. If `send()` doesn't checkpoint after advancing the
    // sender ratchet, the cold-restarted A rolls back to the pre-send generation and
    // the next send re-uses an already-consumed key that B silently rejects.
    //
    // This is the exact symptom users hit on the web: "user 1 reloads, sends a new
    // message, the send succeeds but user 2 never sees it" — the receiver's MLS layer
    // dropped the duplicate-generation envelope without surfacing an error.
    let tmp = TempDir::new().expect("tempdir");
    let a_path = tmp.path().join("a.sqlite");
    let b_path = tmp.path().join("b.sqlite");

    let transport_a = Arc::new(CapturingTransport::default());
    let transport_b = Arc::new(CapturingTransport::default());

    let a_identity_export = Identity::generate().export();
    let b_identity_export = Identity::generate().export();
    let a_host = Arc::new(MemStorage::default()) as Arc<dyn Storage>;
    let b_host = Arc::new(MemStorage::default()) as Arc<dyn Storage>;

    let client_a = open_client(
        "A",
        transport_a.clone(),
        a_host.clone(),
        Identity::import(&a_identity_export).expect("a identity"),
        StorageBackend::Sqlite {
            path: a_path.clone(),
            encryption_key: None,
        },
    );
    let client_b = open_client(
        "B",
        transport_b.clone(),
        b_host.clone(),
        Identity::import(&b_identity_export).expect("b identity"),
        StorageBackend::Sqlite {
            path: b_path.clone(),
            encryption_key: None,
        },
    );

    let conv = block_on(client_a.create_conversation(None, 1_000)).expect("create");
    let kp = client_b.fresh_key_package().expect("kp");
    block_on(client_a.add_members(conv, vec![(client_b.device_id(), kp)], 2_000)).expect("add B");
    let welcome = transport_a
        .sent
        .lock()
        .iter()
        .rev()
        .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");

    // First send (warms A's sender ratchet and produces gen 0).
    let env_first = block_on(client_a.send(conv, b"first".to_vec(), 4_000)).expect("A send first");
    let msg_first = block_on(client_b.process_envelope(&env_first, 5_000))
        .expect("B decrypt first")
        .expect("application msg");
    assert_eq!(msg_first.plaintext, b"first");

    // Cold-restart A — the new client opens against the same on-disk SQLite + same
    // identity + same host KV. A2's sender ratchet must continue from where A left off.
    drop(client_a);
    let client_a2 = open_client(
        "A-rev",
        transport_a.clone(),
        a_host.clone(),
        Identity::import(&a_identity_export).expect("a identity 2"),
        StorageBackend::Sqlite {
            path: a_path.clone(),
            encryption_key: None,
        },
    );

    // Second send AFTER the restart. If `send()` checkpointed after the first send,
    // the ratchet is at gen 1 and B accepts. If it didn't, gen 0 is re-used and B
    // drops the envelope as an already-consumed generation.
    let env_second =
        block_on(client_a2.send(conv, b"second".to_vec(), 6_000)).expect("A2 send second");
    let msg_second = block_on(client_b.process_envelope(&env_second, 7_000))
        .expect("B decrypt second — sender ratchet must survive A's cold restart")
        .expect("application msg");
    assert_eq!(
        msg_second.plaintext, b"second",
        "second send after sender cold-restart MUST decrypt — checkpoint-on-send invariant"
    );
}

#[test]
fn sqlite_wrong_encryption_key_is_rejected() {
    // Open + checkpoint with one key, then try to re-open with a different key.
    // SQLCipher rejects the wrong key at first SQL statement; our PersistentBackend
    // bubbles that up as `EncryptionKeyMismatch` → `Error::Storage`.
    let tmp = TempDir::new().expect("tempdir");
    let path = tmp.path().join("encrypted.sqlite");

    let key1 = zeroize::Zeroizing::new([0x42u8; 32]);
    let key2 = zeroize::Zeroizing::new([0x99u8; 32]);

    let transport = Arc::new(CapturingTransport::default());
    let identity_export = Identity::generate().export();
    let host = Arc::new(MemStorage::default()) as Arc<dyn Storage>;

    {
        let client = open_client(
            "encrypted",
            transport.clone(),
            host.clone(),
            Identity::import(&identity_export).expect("identity"),
            StorageBackend::Sqlite {
                path: path.clone(),
                encryption_key: Some(key1.clone()),
            },
        );
        // Force a checkpoint by creating a conversation.
        let _conv = block_on(client.create_conversation(None, 1_000)).expect("create");
    }

    // Wrong key.
    let cfg = ClientConfig {
        identity: Identity::import(&identity_export).expect("identity 2"),
        device_label: "wrong-key".into(),
        storage: host,
        transport,
        now_ms: 2_000,
        storage_backend: StorageBackend::Sqlite {
            path: path.clone(),
            encryption_key: Some(key2),
        },
        device_signing_secret_key: None,
    };
    let err = block_on(MessagingClient::init(cfg)).unwrap_err();
    let msg = format!("{err}");
    assert!(
        msg.contains("encryption") || msg.contains("key mismatch") || msg.contains("Storage"),
        "wrong SQLCipher key must surface a storage error — got: {msg}"
    );
}
