import { describe, expect, test, afterEach } from 'bun:test';
import {
  createWorkerCryptoProvider,
  SubtleCryptoProvider,
  WorkerCryptoProvider,
} from '../src/index.js';

const WORKER_URL = new URL('../src/worker.ts', import.meta.url);

const subtle = new SubtleCryptoProvider();
let provider: WorkerCryptoProvider | null = null;

afterEach(async () => {
  if (provider) {
    await provider.destroy();
    provider = null;
  }
});

async function makeProvider(idleTimeoutMs = 30_000): Promise<WorkerCryptoProvider> {
  provider = await createWorkerCryptoProvider({
    workerUrl: WORKER_URL,
    idleTimeoutMs,
  });
  return provider;
}

describe('WorkerCryptoProvider — roundtrip and parity', () => {
  test('handshake completes', async () => {
    const p = await makeProvider();
    expect(p).toBeInstanceOf(WorkerCryptoProvider);
  });

  test('AES-GCM encrypt → worker, decrypt locally — produces same plaintext', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);
    const plaintext = new TextEncoder().encode('hello shade workers — large enough payload');

    const enc = await p.aesGcmEncrypt(key, plaintext);
    expect(enc.nonce.length).toBe(12);

    // Decrypt with the local SubtleCryptoProvider — proves wire compatibility
    const dec = await subtle.aesGcmDecrypt(key, enc.ciphertext, enc.nonce);
    expect(dec).toEqual(plaintext);
  });

  test('AES-GCM with AAD round-trips through worker', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);
    const plaintext = subtle.randomBytes(1024);
    const aad = subtle.randomBytes(16);

    const enc = await p.aesGcmEncrypt(key, plaintext, aad);
    const dec = await p.aesGcmDecrypt(key, enc.ciphertext, enc.nonce, aad);
    expect(dec).toEqual(plaintext);
  });

  test('AES-GCM decrypt rejects tampered ciphertext', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);
    const plaintext = new TextEncoder().encode('untampered');
    const enc = await p.aesGcmEncrypt(key, plaintext);
    enc.ciphertext[0]! ^= 0x01;
    await expect(p.aesGcmDecrypt(key, enc.ciphertext, enc.nonce)).rejects.toThrow();
  });

  test('HKDF parity with SubtleCryptoProvider', async () => {
    const p = await makeProvider();
    const ikm = subtle.randomBytes(32);
    const salt = subtle.randomBytes(16);
    const info = new TextEncoder().encode('test info');

    const a = await p.hkdf(ikm, salt, info, 64);
    const b = await subtle.hkdf(ikm, salt, info, 64);
    expect(a).toEqual(b);
  });

  test('HMAC-SHA256 parity with SubtleCryptoProvider', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);
    const data = subtle.randomBytes(256);

    const a = await p.hmacSha256(key, data);
    const b = await subtle.hmacSha256(key, data);
    expect(a).toEqual(b);
  });

  test('X25519 DH agrees with SubtleCryptoProvider', async () => {
    const p = await makeProvider();
    const alice = await p.generateX25519KeyPair();
    const bob = await subtle.generateX25519KeyPair();

    const ab = await p.x25519(alice.privateKey, bob.publicKey);
    const ba = await subtle.x25519(bob.privateKey, alice.publicKey);
    expect(ab).toEqual(ba);
  });

  test('Ed25519 sign in worker, verify locally', async () => {
    const p = await makeProvider();
    const kp = await p.generateEd25519KeyPair();
    const msg = new TextEncoder().encode('please sign me');

    const sig = await p.sign(kp.privateKey, msg);
    expect(await subtle.verify(kp.publicKey, msg, sig)).toBe(true);
  });

  test('Ed25519 verify rejects tampered signature', async () => {
    const p = await makeProvider();
    const kp = await subtle.generateEd25519KeyPair();
    const msg = new TextEncoder().encode('msg');
    const sig = await subtle.sign(kp.privateKey, msg);
    sig[0]! ^= 0x01;
    expect(await p.verify(kp.publicKey, msg, sig)).toBe(false);
  });

  test('local sync helpers do not round-trip', async () => {
    const p = await makeProvider();
    const a = p.randomBytes(16);
    expect(a.length).toBe(16);
    expect(p.constantTimeEqual(a, a)).toBe(true);
    expect(p.constantTimeEqual(a, new Uint8Array(16))).toBe(false);
    expect(typeof p.randomUint32()).toBe('number');
  });

  test('errors from worker propagate as rejected promises', async () => {
    const p = await makeProvider();
    const wrongKey = subtle.randomBytes(32);
    const ct = subtle.randomBytes(48);
    const nonce = subtle.randomBytes(12);
    await expect(p.aesGcmDecrypt(wrongKey, ct, nonce)).rejects.toThrow();
  });

  test('parallel calls do not interleave incorrectly', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);

    const inputs = Array.from({ length: 16 }, (_, i) =>
      new TextEncoder().encode(`payload-${i}-${'x'.repeat(50 * i)}`),
    );

    const encs = await Promise.all(inputs.map((pt) => p.aesGcmEncrypt(key, pt)));
    const decs = await Promise.all(
      encs.map((e) => p.aesGcmDecrypt(key, e.ciphertext, e.nonce)),
    );
    decs.forEach((d, i) => expect(d).toEqual(inputs[i]!));
  });

  test('after destroy(), calls reject', async () => {
    const p = await makeProvider();
    await p.destroy();
    await expect(p.aesGcmEncrypt(subtle.randomBytes(32), new Uint8Array(8))).rejects.toThrow(
      /destroyed/,
    );
    provider = null;
  });

  test('rotate() respawns transparently', async () => {
    const p = await makeProvider();
    const key = subtle.randomBytes(32);
    await p.aesGcmEncrypt(key, new Uint8Array(8));
    await p.rotate();
    const out = await p.aesGcmEncrypt(key, new TextEncoder().encode('still works'));
    const dec = await subtle.aesGcmDecrypt(key, out.ciphertext, out.nonce);
    expect(new TextDecoder().decode(dec)).toBe('still works');
  });

  test('idle-timeout terminates worker but next call respawns', async () => {
    const p = await makeProvider(120);
    const key = subtle.randomBytes(32);
    await p.aesGcmEncrypt(key, new Uint8Array(8));
    // Wait for the idle timer to fire.
    await new Promise((r) => setTimeout(r, 250));
    // Next call should still succeed — proves respawn works.
    const out = await p.aesGcmEncrypt(key, new TextEncoder().encode('respawned'));
    const dec = await subtle.aesGcmDecrypt(key, out.ciphertext, out.nonce);
    expect(new TextDecoder().decode(dec)).toBe('respawned');
  });

  test('configureWorkerCrypto throws on protocol mismatch', async () => {
    // Spawn with a fake "spawn" that returns a worker echoing the wrong version.
    const fakeProvider = new WorkerCryptoProvider({
      workerUrl: WORKER_URL,
      spawn: () => {
        type Listener = (ev: { data: unknown }) => void;
        const listeners: Listener[] = [];
        return {
          postMessage(msg: unknown): void {
            const m = msg as { id: number; method: string };
            if (m.method === 'init') {
              setTimeout(() => {
                for (const l of listeners) {
                  l({
                    data: {
                      id: m.id,
                      ok: false,
                      error: { name: 'Error', message: 'protocol version mismatch' },
                    },
                  });
                }
              }, 0);
            }
          },
          addEventListener(type: string, listener: Listener): void {
            if (type === 'message') listeners.push(listener);
          },
          removeEventListener(): void {
            // no-op
          },
          terminate(): void {
            // no-op
          },
        };
      },
    });
    await expect(fakeProvider.handshake()).rejects.toThrow(/protocol/);
    await fakeProvider.destroy();
  });
});
