import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { sha256 } from '@noble/hashes/sha2.js';
import { NotFoundError, type FileEntry } from '../../src/index.js';
import { setupFileRig, type FileTestRig } from './helpers/rig.js';

interface StoredBlob {
  bytes: Uint8Array;
  contentType?: string;
  sha256: string;
}

function bytesToHex(b: Uint8Array): string {
  return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
}

async function streamToBytes(s: ReadableStream<Uint8Array>): Promise<Uint8Array> {
  const reader = s.getReader();
  const parts: Uint8Array[] = [];
  let total = 0;
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    if (value === undefined) continue;
    parts.push(value);
    total += value.byteLength;
  }
  reader.releaseLock();
  const out = new Uint8Array(total);
  let offset = 0;
  for (const p of parts) {
    out.set(p, offset);
    offset += p.byteLength;
  }
  return out;
}

function streamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {
  return new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(bytes);
      controller.close();
    },
  });
}

describe('Content I/O — streamed read/write E2E (>256 KiB)', () => {
  let rig: FileTestRig;
  const blobs = new Map<string, StoredBlob>();

  beforeAll(async () => {
    blobs.clear();
    rig = await setupFileRig({
      read: async (ctx) => {
        const blob = blobs.get(ctx.path);
        if (blob === undefined) throw new NotFoundError(ctx.path);
        // Return as streams when blob ≥ 256 KiB.
        if (blob.bytes.byteLength > 256 * 1024) {
          return blob.contentType !== undefined
            ? {
                kind: 'streams',
                stream: streamFromBytes(blob.bytes),
                size: blob.bytes.byteLength,
                sha256: blob.sha256,
                contentType: blob.contentType,
              }
            : {
                kind: 'streams',
                stream: streamFromBytes(blob.bytes),
                size: blob.bytes.byteLength,
                sha256: blob.sha256,
              };
        }
        return blob.contentType !== undefined
          ? { kind: 'inline', bytes: blob.bytes, sha256: blob.sha256, contentType: blob.contentType }
          : { kind: 'inline', bytes: blob.bytes, sha256: blob.sha256 };
      },
      write: async (ctx) => {
        const args = ctx.args;
        let bytes: Uint8Array;
        let resolvedSha: string;
        if (args.content.kind === 'inline') {
          bytes = args.content.bytes;
          resolvedSha = args.content.sha256;
        } else {
          bytes = await streamToBytes(args.content.stream);
          resolvedSha = await args.content.sha256;
          if (bytes.byteLength !== args.content.size) {
            throw new Error(`stream produced ${bytes.byteLength} bytes; declared ${args.content.size}`);
          }
        }
        const stored: StoredBlob = {
          bytes,
          ...(args.contentType !== undefined ? { contentType: args.contentType } : {}),
          sha256: resolvedSha,
        };
        blobs.set(args.path, stored);
        const entry: FileEntry = {
          name: args.path.split('/').filter(Boolean).pop() ?? '',
          kind: 'file',
          size: bytes.byteLength,
          mtime: Date.now(),
          ...(args.contentType !== undefined ? { contentType: args.contentType } : {}),
          metadata: { sha256: resolvedSha },
        };
        return { entry };
      },
    });
  });

  afterAll(async () => {
    await rig.teardown();
  });

  test('write 1 MiB streamed → server sees streams content + sha256 matches', async () => {
    const data = new Uint8Array(1024 * 1024);
    crypto.getRandomValues(data);
    const expectedSha = bytesToHex(sha256(data));

    const writeResult = await rig.fs.write('/big1mb.bin', data);
    expect(writeResult.entry.size).toBe(data.byteLength);
    expect(writeResult.entry.metadata.sha256).toBe(expectedSha);
  });

  test('read 1 MiB streamed → client gets streams output, drains correctly', async () => {
    const out = await rig.fs.read('/big1mb.bin');
    expect(out.kind).toBe('streams');
    if (out.kind === 'streams') {
      expect(out.size).toBe(1024 * 1024);
      const drained = await streamToBytes(out.stream);
      expect(drained.byteLength).toBe(1024 * 1024);
      expect(bytesToHex(sha256(drained))).toBe(out.sha256);
      await out.done();
    }
  });

  test('boundary 256 KiB + 1 → streams write + read round-trip', async () => {
    const data = new Uint8Array(256 * 1024 + 1);
    for (let i = 0; i < data.length; i++) data[i] = (i * 31) & 0xff;
    const expectedSha = bytesToHex(sha256(data));

    await rig.fs.write('/boundary-plus-one.bin', data);
    const out = await rig.fs.read('/boundary-plus-one.bin');
    expect(out.kind).toBe('streams');
    if (out.kind === 'streams') {
      expect(out.sha256).toBe(expectedSha);
      const drained = await streamToBytes(out.stream);
      expect(drained.byteLength).toBe(256 * 1024 + 1);
      expect(Array.from(drained.slice(0, 4))).toEqual(Array.from(data.slice(0, 4)));
      expect(Array.from(drained.slice(-4))).toEqual(Array.from(data.slice(-4)));
      await out.done();
    }
  });

  test('write streams via { stream, size } wrapper → ok', async () => {
    const data = new Uint8Array(500 * 1024);
    crypto.getRandomValues(data);
    const expectedSha = bytesToHex(sha256(data));
    const wrapped = { stream: streamFromBytes(data), size: data.byteLength, contentType: 'application/octet-stream' };
    const result = await rig.fs.write('/wrapped.bin', wrapped);
    expect(result.entry.size).toBe(data.byteLength);
    expect(result.entry.metadata.sha256).toBe(expectedSha);
    expect(result.entry.contentType).toBe('application/octet-stream');
  });

  test('write streams from Blob > 256 KiB → ok', async () => {
    const data = new Uint8Array(400 * 1024);
    crypto.getRandomValues(data);
    const expectedSha = bytesToHex(sha256(data));
    const blob = new Blob([data], { type: 'image/png' });
    const result = await rig.fs.write('/blobby-big.png', blob);
    expect(result.entry.size).toBe(data.byteLength);
    expect(result.entry.metadata.sha256).toBe(expectedSha);
    expect(result.entry.contentType).toBe('image/png');
  });
});
