import { describe, test, expect } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import {
  StreamSender,
  StreamReceiver,
  StreamReplayError,
  StreamOutOfOrderError,
  generateStreamId,
  generateStreamSecret,
} from '../src/index.js';

const crypto = new SubtleCryptoProvider();

async function pair() {
  const streamId = generateStreamId(crypto);
  const streamSecret = generateStreamSecret(crypto);
  const sender = await StreamSender.create({ crypto, streamId, streamSecret, laneId: 0 });
  const receiver = await StreamReceiver.create({ crypto, streamId, streamSecret, laneId: 0 });
  return { sender, receiver };
}

describe('Replay and out-of-order detection', () => {
  test('replaying the same chunk twice → StreamReplayError', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('first'), false);
    await receiver.decryptChunk(bytes);
    await expect(receiver.decryptChunk(bytes)).rejects.toThrow(StreamReplayError);
  });

  test('out-of-order chunk (skipping seq) → StreamOutOfOrderError', async () => {
    const { sender, receiver } = await pair();
    await sender.encryptChunk(new TextEncoder().encode('a'), false); // seq 0
    const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), false); // seq 1
    // Skip seq 0; send seq 1 first
    await expect(receiver.decryptChunk(c1)).rejects.toThrow(StreamOutOfOrderError);
  });

  test('error contains expected and received seq', async () => {
    const { sender, receiver } = await pair();
    await sender.encryptChunk(new TextEncoder().encode('skip'), false); // seq 0 produced
    const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('next'), false);
    try {
      await receiver.decryptChunk(c1);
      throw new Error('expected throw');
    } catch (err) {
      expect((err as Error).message).toContain('expected seq=0');
      expect((err as Error).message).toContain('got 1');
    }
  });

  test('out-of-order then in-order: in-order chunk (after error) still works', async () => {
    const { sender, receiver } = await pair();
    const { bytes: c0 } = await sender.encryptChunk(new TextEncoder().encode('a'), false);
    const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), true);
    await expect(receiver.decryptChunk(c1)).rejects.toThrow(StreamOutOfOrderError);
    // Receiver state still expects seq 0 (the error did not advance it)
    const dec0 = await receiver.decryptChunk(c0);
    expect(dec0.seq).toBe(0);
    const dec1 = await receiver.decryptChunk(c1);
    expect(dec1.seq).toBe(1);
  });

  test('replay after a different in-order chunk advanced seq → StreamReplayError', async () => {
    const { sender, receiver } = await pair();
    const { bytes: c0 } = await sender.encryptChunk(new TextEncoder().encode('a'), false);
    const { bytes: c1 } = await sender.encryptChunk(new TextEncoder().encode('b'), false);
    await receiver.decryptChunk(c0);
    await receiver.decryptChunk(c1);
    await expect(receiver.decryptChunk(c0)).rejects.toThrow(StreamReplayError);
  });
});
