import { describe, it, expect } from 'vitest'
import { isStorageError, type StorageAdapter } from './storage.js'

async function readStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
  const reader = stream.getReader()
  const chunks: Uint8Array[] = []
  for (;;) {
    const { done, value } = await reader.read()
    if (done) break
    if (value) chunks.push(value)
  }
  const total = chunks.reduce((n, c) => n + c.length, 0)
  const out = new Uint8Array(total)
  let off = 0
  for (const c of chunks) {
    out.set(c, off)
    off += c.length
  }
  return out
}

/**
 * The single behavioral contract every StorageAdapter must satisfy.
 * `makeAdapter` returns a FRESH empty adapter per call.
 */
export function runStorageConformance(name: string, makeAdapter: () => StorageAdapter): void {
  describe(`StorageAdapter conformance — ${name}`, () => {
    const bytes = new Uint8Array([1, 2, 3, 4, 5])

    it('put→get round-trips the body and reports size/etag', async () => {
      const s = makeAdapter()
      const put = await s.put('media/a.bin', bytes, { contentType: 'application/octet-stream' })
      expect(put.key).toBe('media/a.bin')
      expect(put.size).toBe(5)
      expect(put.etag).toBeTruthy()
      const got = await s.get('media/a.bin')
      expect(got).not.toBeNull()
      expect(await readStream(got!.body)).toEqual(bytes)
      expect(got!.contentType).toBe('application/octet-stream')
    })

    it('get/head return null for an absent key (no enumeration oracle, never throw)', async () => {
      const s = makeAdapter()
      expect(await s.get('media/missing')).toBeNull()
      expect(await s.head('media/missing')).toBeNull()
    })

    it('head returns metadata without a body', async () => {
      const s = makeAdapter()
      await s.put('media/b.bin', bytes)
      const head = await s.head('media/b.bin')
      expect(head?.size).toBe(5)
      expect((head as unknown as { body?: unknown }).body).toBeUndefined()
    })

    it('delete is idempotent (absent key resolves, never throws)', async () => {
      const s = makeAdapter()
      await s.put('media/c.bin', bytes)
      await s.delete('media/c.bin')
      await expect(s.delete('media/c.bin')).resolves.toBeUndefined()
      expect(await s.get('media/c.bin')).toBeNull()
    })

    it('list paginates by prefix + cursor', async () => {
      const s = makeAdapter()
      await s.put('media/1', bytes)
      await s.put('media/2', bytes)
      await s.put('other/3', bytes)
      const page1 = await s.list({ prefix: 'media/', limit: 1 })
      expect(page1.objects).toHaveLength(1)
      expect(page1.cursor).toBeTruthy()
      const page2 = await s.list({ prefix: 'media/', limit: 1, cursor: page1.cursor })
      expect(page2.objects).toHaveLength(1)
      const keys = [page1.objects[0]!.key, page2.objects[0]!.key].sort()
      expect(keys).toEqual(['media/1', 'media/2'])
    })

    it('presignPut rejects a non-exact key with code invalid_key', async () => {
      const s = makeAdapter()
      await expect(s.presignPut('media/*', { expiresIn: 60 })).rejects.toSatisfy(
        (e: unknown) => isStorageError(e) && e.code === 'invalid_key',
      )
    })

    it('presignPut rejects a non-positive expiry', async () => {
      const s = makeAdapter()
      await expect(s.presignPut('media/ok', { expiresIn: 0 })).rejects.toBeInstanceOf(RangeError)
    })

    it('presignPut returns a bounded PUT capability for an exact key', async () => {
      const s = makeAdapter()
      const url = await s.presignPut('media/ok.bin', { expiresIn: 120, contentType: 'image/png' })
      expect(url.method).toBe('PUT')
      expect(url.expiresIn).toBe(120)
      expect(url.url).toBeTruthy()
    })
  })
}
