import { describe, expect, test } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { createRecorder } from '@shade/observability';
import {
  TransferEngine,
  MemoryControlChannel,
  MemoryTransferTransport,
  type IncomingTransfer,
  type TransferHandle,
  type TransferResult,
} from '../src/index.js';

const crypto = new SubtleCryptoProvider();

const ALICE = 'alice@trace-test.local';
const BOB = 'bob@trace-test.local';
const SECRET_PAYLOAD = new TextEncoder().encode(
  'classified-shadow-token-DO-NOT-LOG',
);

function makePair(observability: ReturnType<typeof createRecorder>) {
  const { a: ctrlA, b: ctrlB } = MemoryControlChannel.linked(ALICE, BOB);
  const { a: txA, b: txB } = MemoryTransferTransport.linked(ALICE, BOB);
  const sender = new TransferEngine({
    crypto,
    controlChannel: ctrlA,
    transport: txA,
    myAddress: ALICE,
    observability,
  });
  const receiver = new TransferEngine({
    crypto,
    controlChannel: ctrlB,
    transport: txB,
    myAddress: BOB,
    observability,
  });
  txB.setChunkHandler(async (from, sid, lane, seq, bytes) =>
    receiver.receiveChunk(from, sid, lane, seq, bytes),
  );
  return { sender, receiver };
}

describe('TransferEngine observability', () => {
  test('emits upload+download spans with PII-safe attributes', async () => {
    const rec = createRecorder();
    const { sender, receiver } = makePair(rec);

    let resolveRecv!: (h: TransferHandle) => void;
    const recvP = new Promise<TransferHandle>((r) => { resolveRecv = r; });
    const unsub = receiver.onIncomingTransfer(async (incoming: IncomingTransfer) => {
      const h = await incoming.accept({ output: { kind: 'buffer' } });
      resolveRecv(h);
    });

    const handle = await sender.upload({
      to: BOB,
      input: SECRET_PAYLOAD,
      lanes: 1,
      chunkSize: 4096,
    });
    const recvH = await recvP;
    await Promise.all([handle.done(), recvH.done()]);
    unsub();

    const upload = rec.spans.find((s) => s.name === 'shade.transfer.upload');
    const download = rec.spans.find((s) => s.name === 'shade.transfer.download');
    expect(upload).toBeDefined();
    expect(download).toBeDefined();

    expect(upload!.attributes['shade.direction']).toBe('upload');
    expect(upload!.attributes['shade.peer.hash']).toMatch(/^[0-9a-f]{8}$/);
    expect(upload!.attributes['shade.bytes.bin']).toBe('≤4KB');
    expect(upload!.attributes['shade.lane.count']).toBe(1);
    expect(upload!.attributes['shade.result']).toBe('ok');
    expect(upload!.status).toBe('ok');
    expect(upload!.ended).toBe(true);

    expect(download!.attributes['shade.direction']).toBe('download');
    expect(download!.attributes['shade.peer.hash']).toMatch(/^[0-9a-f]{8}$/);
    expect(download!.attributes['shade.result']).toBe('ok');
    expect(download!.ended).toBe(true);

    // PII guard: no plaintext addresses, no payload, no exact byte counts.
    const hits = rec.scanForPII([
      ALICE,
      BOB,
      'trace-test',
      'classified-shadow-token',
      String(SECRET_PAYLOAD.length),
    ]);
    if (hits.length > 0) {
      throw new Error(`PII leak: ${JSON.stringify(hits, null, 2)}`);
    }
  });

  test('failed upload marks span as error', async () => {
    const rec = createRecorder();
    const { sender } = makePair(rec);
    // No receiver wired, so probe will succeed but accept() never fires.
    // Force failure by aborting before the upload completes.
    const ac = new AbortController();
    const handle = await sender.upload({
      to: BOB,
      input: new Uint8Array(8),
      lanes: 1,
      chunkSize: 4,
      signal: ac.signal,
    });
    ac.abort();
    await handle.done().catch(() => undefined);
    const upload = rec.spans.find((s) => s.name === 'shade.transfer.upload');
    expect(upload).toBeDefined();
    expect(upload!.ended).toBe(true);
    // Either resulted in error or completed cleanly before abort took effect.
    // We only assert that attributes never echoed peer addresses.
    const hits = rec.scanForPII([ALICE, BOB, 'trace-test']);
    if (hits.length > 0) {
      throw new Error(`PII leak (failure path): ${JSON.stringify(hits, null, 2)}`);
    }
  });
});

// Type-level guard so TransferResult import isn't unused.
const _t: TransferResult | null = null;
void _t;
