import { describe, test, expect } from 'bun:test';
import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web';
import {
  generateIdentityKeyPair,
  generateSignedPreKey,
  generateOneTimePreKeys,
  createPreKeyBundle,
  processPreKeyBundle,
  processPreKeyMessage,
  initSenderSession,
  initReceiverSession,
  ratchetEncrypt,
  ratchetDecrypt,
} from '../src/index.js';

const crypto = new SubtleCryptoProvider();
const enc = new TextEncoder();
const dec = new TextDecoder();

describe('Full E2EE Integration: X3DH → Double Ratchet', () => {
  test('complete conversation between Alice and Bob', async () => {
    // ─── Setup Bob (publishes prekey bundle) ─────────────────
    const bobStorage = new MemoryStorage();
    const bobIdentity = await generateIdentityKeyPair(crypto);
    await bobStorage.saveIdentityKeyPair(bobIdentity);
    await bobStorage.saveLocalRegistrationId(42);

    const bobSignedPreKey = await generateSignedPreKey(crypto, bobIdentity, 1);
    await bobStorage.saveSignedPreKey(bobSignedPreKey);

    const bobOTPKs = await generateOneTimePreKeys(crypto, 100, 10);
    for (const otpk of bobOTPKs) await bobStorage.saveOneTimePreKey(otpk);

    const bundle = createPreKeyBundle(42, bobIdentity, bobSignedPreKey, bobOTPKs[0]);

    // ─── Alice initiates (processes bundle, creates session) ──
    const aliceStorage = new MemoryStorage();
    const aliceIdentity = await generateIdentityKeyPair(crypto);
    await aliceStorage.saveIdentityKeyPair(aliceIdentity);

    const x3dhResult = await processPreKeyBundle(crypto, aliceStorage, bundle);

    // Alice initializes her ratchet session
    const aliceSession = await initSenderSession(
      crypto,
      x3dhResult.rootKey,
      x3dhResult.remoteIdentityKey,
      x3dhResult.remoteSignedPreKey, // Bob's signed prekey = initial DH ratchet key
    );

    // Alice encrypts her first message
    const firstMsg = await ratchetEncrypt(crypto, aliceSession, enc.encode('Hello Bob! This is E2EE.'));

    // Alice sends a PreKeyMessage to Bob
    const preKeyMessage = {
      registrationId: 1,
      preKeyId: x3dhResult.preKeyId,
      signedPreKeyId: x3dhResult.signedPreKeyId,
      ephemeralKey: x3dhResult.ephemeralPublicKey,
      identityDHKey: aliceIdentity.dhPublicKey,
      message: firstMsg,
    };

    // ─── Bob receives and processes ──────────────────────────
    const bobX3dh = await processPreKeyMessage(crypto, bobStorage, preKeyMessage);
    expect(bobX3dh.rootKey).toEqual(x3dhResult.rootKey);

    // Bob initializes his ratchet session
    const bobSession = initReceiverSession(
      bobX3dh.rootKey,
      bobX3dh.remoteIdentityKey,
      bobSignedPreKey.keyPair, // Bob's signed prekey as his initial DH keypair
    );

    // Bob decrypts Alice's first message
    const plaintext1 = await ratchetDecrypt(crypto, bobSession, bobX3dh.initialMessage);
    expect(dec.decode(plaintext1)).toBe('Hello Bob! This is E2EE.');

    // ─── Full conversation ───────────────────────────────────

    // Alice sends more
    const m2 = await ratchetEncrypt(crypto, aliceSession, enc.encode('Are you there?'));
    expect(dec.decode(await ratchetDecrypt(crypto, bobSession, m2))).toBe('Are you there?');

    // Bob replies (DH ratchet triggers)
    const m3 = await ratchetEncrypt(crypto, bobSession, enc.encode('Yes! Forward secrecy is active.'));
    expect(dec.decode(await ratchetDecrypt(crypto, aliceSession, m3))).toBe('Yes! Forward secrecy is active.');

    // Alice replies
    const m4 = await ratchetEncrypt(crypto, aliceSession, enc.encode('Every message has a unique key.'));
    expect(dec.decode(await ratchetDecrypt(crypto, bobSession, m4))).toBe('Every message has a unique key.');

    // Multiple back-and-forth
    for (let i = 0; i < 10; i++) {
      const sender = i % 2 === 0 ? aliceSession : bobSession;
      const receiver = i % 2 === 0 ? bobSession : aliceSession;
      const text = `Turn ${i}: ${i % 2 === 0 ? 'Alice' : 'Bob'} speaking`;

      const msg = await ratchetEncrypt(crypto, sender, enc.encode(text));
      expect(dec.decode(await ratchetDecrypt(crypto, receiver, msg))).toBe(text);
    }
  });

  test('works without one-time prekey', async () => {
    const bobStorage = new MemoryStorage();
    const bobIdentity = await generateIdentityKeyPair(crypto);
    await bobStorage.saveIdentityKeyPair(bobIdentity);
    const bobSignedPreKey = await generateSignedPreKey(crypto, bobIdentity, 1);
    await bobStorage.saveSignedPreKey(bobSignedPreKey);

    // No one-time prekeys
    const bundle = createPreKeyBundle(42, bobIdentity, bobSignedPreKey);

    const aliceStorage = new MemoryStorage();
    const aliceIdentity = await generateIdentityKeyPair(crypto);
    await aliceStorage.saveIdentityKeyPair(aliceIdentity);

    const x3dhResult = await processPreKeyBundle(crypto, aliceStorage, bundle);
    const aliceSession = await initSenderSession(
      crypto, x3dhResult.rootKey, x3dhResult.remoteIdentityKey, x3dhResult.remoteSignedPreKey,
    );

    const firstMsg = await ratchetEncrypt(crypto, aliceSession, enc.encode('No OTPK needed'));

    const preKeyMessage = {
      registrationId: 1,
      signedPreKeyId: x3dhResult.signedPreKeyId,
      ephemeralKey: x3dhResult.ephemeralPublicKey,
      identityDHKey: aliceIdentity.dhPublicKey,
      message: firstMsg,
    };

    const bobX3dh = await processPreKeyMessage(crypto, bobStorage, preKeyMessage);
    const bobSession = initReceiverSession(
      bobX3dh.rootKey, bobX3dh.remoteIdentityKey, bobSignedPreKey.keyPair,
    );

    expect(dec.decode(await ratchetDecrypt(crypto, bobSession, bobX3dh.initialMessage)))
      .toBe('No OTPK needed');

    // Continue conversation
    const reply = await ratchetEncrypt(crypto, bobSession, enc.encode('Got it!'));
    expect(dec.decode(await ratchetDecrypt(crypto, aliceSession, reply))).toBe('Got it!');
  });

  test('one-time prekey consumed after use', async () => {
    const bobStorage = new MemoryStorage();
    const bobIdentity = await generateIdentityKeyPair(crypto);
    await bobStorage.saveIdentityKeyPair(bobIdentity);
    const bobSignedPreKey = await generateSignedPreKey(crypto, bobIdentity, 1);
    await bobStorage.saveSignedPreKey(bobSignedPreKey);
    const bobOTPKs = await generateOneTimePreKeys(crypto, 100, 3);
    for (const otpk of bobOTPKs) await bobStorage.saveOneTimePreKey(otpk);

    expect(await bobStorage.getOneTimePreKeyCount()).toBe(3);

    // Alice uses OTPK 100
    const bundle = createPreKeyBundle(42, bobIdentity, bobSignedPreKey, bobOTPKs[0]);
    const aliceStorage = new MemoryStorage();
    const aliceIdentity = await generateIdentityKeyPair(crypto);
    await aliceStorage.saveIdentityKeyPair(aliceIdentity);

    const x3dhResult = await processPreKeyBundle(crypto, aliceStorage, bundle);
    const aliceSession = await initSenderSession(
      crypto, x3dhResult.rootKey, x3dhResult.remoteIdentityKey, x3dhResult.remoteSignedPreKey,
    );
    const firstMsg = await ratchetEncrypt(crypto, aliceSession, enc.encode('test'));

    await processPreKeyMessage(crypto, bobStorage, {
      registrationId: 1,
      preKeyId: 100,
      signedPreKeyId: 1,
      ephemeralKey: x3dhResult.ephemeralPublicKey,
      identityDHKey: aliceIdentity.dhPublicKey,
      message: firstMsg,
    });

    // OTPK 100 consumed, 101 and 102 remain
    expect(await bobStorage.getOneTimePreKeyCount()).toBe(2);
    expect(await bobStorage.getOneTimePreKey(100)).toBeNull();
    expect(await bobStorage.getOneTimePreKey(101)).not.toBeNull();
  });
});
