import { describe, test, expect } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { decodeStreamChunk, encodeStreamChunk } from '@shade/proto';
import {
  StreamSender,
  StreamReceiver,
  StreamDecryptionError,
  StreamProtocolError,
  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('Tamper detection', () => {
  test('flipping a ciphertext byte → StreamDecryptionError', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.ciphertext[0] ^= 0x01;
    const reencoded = encodeStreamChunk(env);
    await expect(receiver.decryptChunk(reencoded)).rejects.toThrow(StreamDecryptionError);
  });

  test('flipping the AEAD tag → StreamDecryptionError', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.ciphertext[env.ciphertext.length - 1] ^= 0x80;
    await expect(receiver.decryptChunk(encodeStreamChunk(env))).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('tampering with isLast flag → StreamDecryptionError (AAD mismatch)', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.isLast = true;
    await expect(receiver.decryptChunk(encodeStreamChunk(env))).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('tampering with the wire nonce → StreamProtocolError (deterministic check)', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.nonce[0] ^= 0x01;
    await expect(receiver.decryptChunk(encodeStreamChunk(env))).rejects.toThrow(
      StreamProtocolError,
    );
  });

  test('tampering with streamId → StreamProtocolError', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.streamId[0] ^= 0xff;
    await expect(receiver.decryptChunk(encodeStreamChunk(env))).rejects.toThrow(
      StreamProtocolError,
    );
  });

  test('routing a chunk to wrong-lane receiver → StreamProtocolError', async () => {
    const streamId = generateStreamId(crypto);
    const streamSecret = generateStreamSecret(crypto);
    const sender0 = await StreamSender.create({ crypto, streamId, streamSecret, laneId: 0 });
    const receiver1 = await StreamReceiver.create({ crypto, streamId, streamSecret, laneId: 1 });
    const { bytes } = await sender0.encryptChunk(new TextEncoder().encode('payload'), false);
    await expect(receiver1.decryptChunk(bytes)).rejects.toThrow(StreamProtocolError);
  });

  test('non-empty AAD on wire → StreamProtocolError (reserved in v0.2.0)', async () => {
    const { sender, receiver } = await pair();
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('payload'), false);
    const env = decodeStreamChunk(bytes);
    env.aad = new Uint8Array([1, 2, 3]);
    await expect(receiver.decryptChunk(encodeStreamChunk(env))).rejects.toThrow(
      StreamProtocolError,
    );
  });

  test('different streamSecret → StreamDecryptionError', async () => {
    const streamId = generateStreamId(crypto);
    const sender = await StreamSender.create({
      crypto,
      streamId,
      streamSecret: new Uint8Array(32).fill(1),
      laneId: 0,
    });
    const receiver = await StreamReceiver.create({
      crypto,
      streamId,
      streamSecret: new Uint8Array(32).fill(2),
      laneId: 0,
    });
    const { bytes } = await sender.encryptChunk(new TextEncoder().encode('hi'), false);
    await expect(receiver.decryptChunk(bytes)).rejects.toThrow(StreamDecryptionError);
  });
});
