//! D3.4 — Adversarial proof: membership directory cannot leak DEKs to non-members.
//!
//! ## Two-layer proof (ADR-027 §"Service NEVER holds")
//!
//! Layer 1 — Server-side membership gate (tested here):
//!   `fetch_dek` returns `CollabError::NotMember` for any pubkey not in the team
//!   directory, regardless of which pubkey is passed as the recipient argument.
//!
//! Layer 2 — Cryptographic backstop (documented, not automated):
//!   Even if a non-member somehow receives a `DekEnvelope`, they cannot decrypt
//!   `age_ciphertext_b64` because the ciphertext is age-encrypted to the
//!   *recipient*'s X25519 public key.  Attempting `age_decrypt(envelope,
//!   wrong_identity)` returns an `age::DecryptError` — the decryption fails at
//!   the recipient-set check before any plaintext bytes are produced.  Automating
//!   this as a test would require generating real age key pairs in the test suite,
//!   which is a heavier dependency than D3.4 calls for.  The proof is therefore
//!   stated here as a design property and left for a dedicated crypto integration
//!   test when real age key generation is added to the test harness.

use chrono::Utc;
use tsafe_collab::{CollabError, CollabRemote, DekEnvelope, StubServer};

fn make_envelope(team_id: &str, recipient: &str) -> DekEnvelope {
    DekEnvelope {
        schema: DekEnvelope::SCHEMA.to_owned(),
        team_id: team_id.to_owned(),
        recipient_pubkey: recipient.to_owned(),
        age_ciphertext_b64: "ZmFrZWNpcGhlcnRleHQ=".to_owned(), // base64("fakeciphertext")
        delivered_at: Utc::now(),
    }
}

/// A pubkey that never called `join` gets `NotMember` on `fetch_dek`.
#[test]
fn non_member_cannot_fetch_dek() {
    let server = StubServer::new();
    let team_id = "team-alpha";
    let member_pubkey = "age1member000000000000000000000000000000000000000000000000000000";
    let outsider_pubkey = "age1outsider0000000000000000000000000000000000000000000000000000";

    // Enroll the member and deliver a DEK envelope to them.
    server.join(team_id, member_pubkey).unwrap();
    server
        .deliver_dek(
            team_id,
            member_pubkey,
            make_envelope(team_id, member_pubkey),
        )
        .unwrap();

    // The outsider was never enrolled — any fetch attempt must fail with NotMember.
    let result = server.fetch_dek(team_id, outsider_pubkey);
    assert!(
        matches!(result, Err(CollabError::NotMember { .. })),
        "expected NotMember, got: {result:?}"
    );
}

/// A non-member passing a *member's* pubkey to `fetch_dek` is still rejected.
///
/// In the stub the "caller identity" is the `recipient_pubkey` parameter; the
/// server-side membership gate uses that value to enforce access.  A real
/// production implementation would additionally bind the request to a bearer
/// token or mTLS certificate — this test exercises the data-model gate that
/// must hold regardless of auth mechanism.
#[test]
fn non_member_impersonation_rejected() {
    let server = StubServer::new();
    let team_id = "team-beta";
    let victim_pubkey = "age1victim000000000000000000000000000000000000000000000000000000";
    let attacker_pubkey = "age1attacker0000000000000000000000000000000000000000000000000000";

    // Only the victim is a member.
    server.join(team_id, victim_pubkey).unwrap();
    server
        .deliver_dek(
            team_id,
            victim_pubkey,
            make_envelope(team_id, victim_pubkey),
        )
        .unwrap();

    // The attacker presents the victim's pubkey but is themselves not enrolled.
    // The membership check uses the supplied pubkey as the caller identity.
    // Since `attacker_pubkey` is not in the member list, this must be NotMember.
    //
    // Note: the attacker cannot impersonate the victim at the *crypto* layer
    // either — they would need the victim's age private key to decrypt the
    // ciphertext even if they somehow obtained the envelope bytes.
    let result = server.fetch_dek(team_id, attacker_pubkey);
    assert!(
        matches!(result, Err(CollabError::NotMember { .. })),
        "expected NotMember for non-member using attacker pubkey, got: {result:?}"
    );
}

/// `confirm_invite` with a wrong confirming pubkey returns `PubkeyMismatch`.
#[test]
fn confirm_invite_pubkey_mismatch() {
    let server = StubServer::new();
    let team_id = "team-gamma";
    let inviter_pubkey = "age1inviter000000000000000000000000000000000000000000000000000000";
    let real_invitee = "age1invitee000000000000000000000000000000000000000000000000000000";
    let wrong_pubkey = "age1wrong0000000000000000000000000000000000000000000000000000000";

    server.join(team_id, inviter_pubkey).unwrap();

    // Inviter creates an invite bound to `real_invitee`'s pubkey.
    let token = server.create_invite(team_id, real_invitee).unwrap();

    // Someone presents the token but with a *different* pubkey — MITM scenario.
    let result = server.confirm_invite(team_id, &token, wrong_pubkey);
    assert!(
        matches!(result, Err(CollabError::PubkeyMismatch)),
        "expected PubkeyMismatch, got: {result:?}"
    );
}

/// Happy path: a correctly enrolled member can fetch their DEK end-to-end.
#[test]
fn correct_member_can_fetch_dek() {
    let server = StubServer::new();
    let team_id = "team-delta";
    let inviter_pubkey = "age1inviter111111111111111111111111111111111111111111111111111111";
    let invitee_pubkey = "age1invitee111111111111111111111111111111111111111111111111111111";

    // Inviter enrolls themselves.
    server.join(team_id, inviter_pubkey).unwrap();

    // Inviter creates an invite for the invitee.
    let token = server.create_invite(team_id, invitee_pubkey).unwrap();
    assert_eq!(token.bound_pubkey, invitee_pubkey);
    assert_eq!(token.team_id, team_id);

    // Invitee confirms the invite with their own pubkey.
    server
        .confirm_invite(team_id, &token, invitee_pubkey)
        .unwrap();

    // Inviter delivers a DEK envelope to the invitee.
    let envelope = make_envelope(team_id, invitee_pubkey);
    let expected_ciphertext = envelope.age_ciphertext_b64.clone();
    server
        .deliver_dek(team_id, invitee_pubkey, envelope)
        .unwrap();

    // Invitee fetches their DEK — must succeed and return the correct ciphertext.
    let fetched = server
        .fetch_dek(team_id, invitee_pubkey)
        .unwrap()
        .expect("DEK envelope should be present after delivery");

    assert_eq!(fetched.schema, DekEnvelope::SCHEMA);
    assert_eq!(fetched.recipient_pubkey, invitee_pubkey);
    assert_eq!(fetched.age_ciphertext_b64, expected_ciphertext);
}
