import { describe, test, expect } from 'bun:test';
import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web';
import {
  ShadeSessionManager,
  ShadeEventEmitter,
  shortHash,
} from '../src/index.js';
import type { ShadeEvent } from '../src/index.js';

const crypto = new SubtleCryptoProvider();

describe('ShadeEventEmitter', () => {
  test('subscribes and emits events', () => {
    const emitter = new ShadeEventEmitter();
    const received: ShadeEvent[] = [];
    emitter.on((e) => received.push(e));

    emitter.emit('identity.initialized', { fingerprint: 'abc', registrationId: 1 });

    expect(received.length).toBe(1);
    expect(received[0]!.name).toBe('identity.initialized');
    expect(received[0]!.seq).toBe(1);
    expect(received[0]!.timestamp).toBeGreaterThan(0);
    expect((received[0]!.data as any).fingerprint).toBe('abc');
  });

  test('seq is monotonically increasing', () => {
    const emitter = new ShadeEventEmitter();
    const seqs: number[] = [];
    emitter.on((e) => seqs.push(e.seq));

    emitter.emit('prekey.generated', { count: 5, totalAfter: 5 });
    emitter.emit('prekey.consumed', { keyId: 1 });
    emitter.emit('prekey.consumed', { keyId: 2 });

    expect(seqs).toEqual([1, 2, 3]);
  });

  test('unsubscribe stops receiving events', () => {
    const emitter = new ShadeEventEmitter();
    let count = 0;
    const unsub = emitter.on(() => count++);
    emitter.emit('prekey.generated', { count: 1, totalAfter: 1 });
    unsub();
    emitter.emit('prekey.generated', { count: 1, totalAfter: 2 });
    expect(count).toBe(1);
  });

  test('listener throw does not break other listeners', () => {
    const emitter = new ShadeEventEmitter();
    let goodCount = 0;
    emitter.on(() => { throw new Error('boom'); });
    emitter.on(() => goodCount++);
    emitter.emit('prekey.generated', { count: 1, totalAfter: 1 });
    expect(goodCount).toBe(1);
  });

  test('getBufferedSince returns events after seq', () => {
    const emitter = new ShadeEventEmitter();
    emitter.emit('prekey.generated', { count: 1, totalAfter: 1 });
    emitter.emit('prekey.generated', { count: 1, totalAfter: 2 });
    emitter.emit('prekey.generated', { count: 1, totalAfter: 3 });
    const events = emitter.getBufferedSince(1);
    expect(events.length).toBe(2);
    expect(events[0]!.seq).toBe(2);
    expect(events[1]!.seq).toBe(3);
  });

  test('ring buffer evicts oldest', () => {
    const emitter = new ShadeEventEmitter({ bufferSize: 3 });
    for (let i = 0; i < 5; i++) {
      emitter.emit('prekey.generated', { count: 1, totalAfter: i });
    }
    const recent = emitter.getRecent(10);
    expect(recent.length).toBe(3);
    expect(recent[0]!.seq).toBe(3);
    expect(recent[2]!.seq).toBe(5);
  });
});

describe('shortHash helper', () => {
  test('produces 16-hex-char string', async () => {
    const hash = await shortHash(crypto, crypto.randomBytes(32));
    expect(hash).toMatch(/^[0-9a-f]{16}$/);
  });

  test('deterministic for same input', async () => {
    const key = new Uint8Array(32).fill(0xab);
    const a = await shortHash(crypto, key);
    const b = await shortHash(crypto, key);
    expect(a).toBe(b);
  });

  test('different inputs produce different hashes', async () => {
    const a = await shortHash(crypto, crypto.randomBytes(32));
    const b = await shortHash(crypto, crypto.randomBytes(32));
    expect(a).not.toBe(b);
  });
});

describe('ShadeSessionManager event integration', () => {
  test('initialize emits identity.initialized', async () => {
    const events = new ShadeEventEmitter();
    const received: ShadeEvent[] = [];
    events.on((e) => received.push(e));

    const mgr = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await mgr.initialize();

    const init = received.find((e) => e.name === 'identity.initialized');
    expect(init).toBeDefined();
    const data = init!.data as any;
    expect(data.fingerprint).toMatch(/^\d{5}( \d{5}){11}$/);
    expect(data.registrationId).toBeGreaterThan(0);
  });

  test('full conversation emits expected event sequence', async () => {
    const events = new ShadeEventEmitter();
    const received: ShadeEvent[] = [];
    events.on((e) => received.push(e));

    const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await alice.initialize();
    await bob.initialize();

    const otpks = await bob.generateOneTimePreKeys(5);
    const bundle = await bob.createPreKeyBundle();
    bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
    await alice.initSessionFromBundle('bob', bundle);

    const env1 = await alice.encrypt('bob', 'hello');
    await bob.decrypt('alice', env1);
    const env2 = await bob.encrypt('alice', 'hi');
    await alice.decrypt('bob', env2);

    const names = received.map((e) => e.name);
    expect(names).toContain('identity.initialized');
    expect(names).toContain('prekey.generated');
    expect(names).toContain('session.created');
    expect(names).toContain('trust.pinned');
    expect(names).toContain('message.encrypted');
    expect(names).toContain('message.decrypted');
    expect(names).toContain('ratchet.dh_step'); // Bob's reply triggers a DH step
  });

  test('no events emitted when emitter not provided', async () => {
    const mgr = new ShadeSessionManager(crypto, new MemoryStorage());
    await mgr.initialize();
    // No assertion needed — should not throw or error
  });

  test('SECURITY: no key material in event payloads', async () => {
    const events = new ShadeEventEmitter();
    const received: ShadeEvent[] = [];
    events.on((e) => received.push(e));

    const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await alice.initialize();
    await bob.initialize();
    const otpks = await bob.generateOneTimePreKeys(5);
    const bundle = await bob.createPreKeyBundle();
    bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
    await alice.initSessionFromBundle('bob', bundle);
    const env = await alice.encrypt('bob', 'secret message');
    await bob.decrypt('alice', env);
    await alice.rotateSignedPreKey();

    // Serialize all events and check for any 32-byte base64 patterns
    // (which would indicate raw key material)
    const json = JSON.stringify(received);

    // 32-byte base64 = 44 chars (with padding) or 43 (without)
    // We allow short 16-hex-char hashes, but no 44-char base64 or 64-char hex
    const longBase64 = /[A-Za-z0-9+/]{43,}={0,2}/g;
    const longHex = /[0-9a-f]{32,}/gi;

    const base64Matches = json.match(longBase64) ?? [];
    const hexMatches = json.match(longHex) ?? [];

    // Filter out any matches that are inside hash fields (which are 16 hex chars,
    // so the regex above wouldn't match anyway, but be explicit)
    expect(base64Matches.length).toBe(0);
    expect(hexMatches.length).toBe(0);

    // Also no plaintext leakage
    expect(json).not.toContain('secret message');
  });
});
