import { describe, it, expect } from 'vitest'
import { runStorageConformance } from './storage-conformance.js'
import { createR2Storage, type R2StorageBucket } from './storage-r2.js'

/** Minimal in-memory R2Bucket double exposing exactly the surface the adapter consumes. */
function fakeR2Bucket(): R2StorageBucket {
  const map = new Map<string, { bytes: Uint8Array; contentType?: string; customMetadata?: Record<string, string> }>()
  const toBytes = async (v: unknown): Promise<Uint8Array> => {
    if (v instanceof Uint8Array) return v
    if (v instanceof ArrayBuffer) return new Uint8Array(v)
    if (ArrayBuffer.isView(v)) return new Uint8Array((v as ArrayBufferView).buffer)
    if (typeof Blob !== 'undefined' && v instanceof Blob) return new Uint8Array(await v.arrayBuffer())
    const reader = (v as ReadableStream<Uint8Array>).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
  }
  const stream = (b: Uint8Array) => new ReadableStream<Uint8Array>({ start(c) { c.enqueue(b); c.close() } })
  return {
    async put(key, value, options) {
      const bytes = await toBytes(value)
      map.set(key, { bytes, contentType: options?.httpMetadata?.contentType, customMetadata: options?.customMetadata })
      return { key, size: bytes.length, etag: `"${bytes.length}"` }
    },
    async get(key) {
      const e = map.get(key)
      if (!e) return null
      return { key, size: e.bytes.length, etag: `"${e.bytes.length}"`, httpMetadata: { contentType: e.contentType }, customMetadata: e.customMetadata, body: stream(e.bytes) }
    },
    async head(key) {
      const e = map.get(key)
      if (!e) return null
      return { key, size: e.bytes.length, etag: `"${e.bytes.length}"`, httpMetadata: { contentType: e.contentType }, customMetadata: e.customMetadata }
    },
    async delete(key) { map.delete(key) },
    async list(opts) {
      const prefix = opts?.prefix ?? ''
      const limit = opts?.limit ?? 1000
      const offset = opts?.cursor ? Number.parseInt(opts.cursor, 10) || 0 : 0
      const all = [...map.entries()].filter(([k]) => k.startsWith(prefix)).sort(([a], [b]) => a.localeCompare(b))
      const page = all.slice(offset, offset + limit)
      const next = offset + limit
      return { objects: page.map(([key, e]) => ({ key, size: e.bytes.length, etag: `"${e.bytes.length}"` })), truncated: next < all.length, cursor: next < all.length ? String(next) : undefined }
    },
  }
}

const PRESIGN = { endpoint: 'https://acct.r2.cloudflarestorage.com/bucket', accessKeyId: 'AK', secretAccessKey: 'SK' }

runStorageConformance('r2', () => createR2Storage({ bucket: fakeR2Bucket(), presign: PRESIGN }))

describe('createR2Storage specifics', () => {
  it('presignPut throws backend when no presign creds are supplied', async () => {
    const s = createR2Storage({ bucket: fakeR2Bucket() })
    await expect(s.presignPut('media/x', { expiresIn: 60 })).rejects.toMatchObject({ code: 'backend' })
  })

  it('does not expose presignGet (public-bucket default)', () => {
    const s = createR2Storage({ bucket: fakeR2Bucket(), presign: PRESIGN })
    expect(s.presignGet).toBeUndefined()
  })
})
