import { describe, it, expect } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useUpload } from './useUpload.js'
import { UploadRejectedError, isUploadInFlightError } from './errors.js'
import type { MediaClient, MediaItem } from './client.js'

const item: MediaItem = { key: 'media/x.png', url: 'https://cdn/x.png', size: 10, width: 2, height: 2 }

function clientOk(): MediaClient {
  return { upload: async () => item, list: async () => ({ items: [] }), remove: async () => {} }
}
function clientProgress(seen: number[]): MediaClient {
  return {
    upload: async (_file, opts) => {
      // The hook MUST forward an onProgress that feeds its `progress` state.
      opts?.onProgress?.(0.5)
      seen.push(0.5)
      return item
    },
    list: async () => ({ items: [] }),
    remove: async () => {},
  }
}
function clientReject(): MediaClient {
  return { upload: async () => { throw new UploadRejectedError('unsupported_type', 415) }, list: async () => ({ items: [] }), remove: async () => {} }
}

const file = new File([new Uint8Array([1, 2])], 'x.png', { type: 'image/png' })

describe('useUpload', () => {
  it('uploads and exposes the resulting item, toggling uploading', async () => {
    const { result } = renderHook(() => useUpload(clientOk()))
    expect(result.current.uploading).toBe(false)
    let returned: MediaItem | undefined
    await act(async () => { returned = await result.current.upload(file) })
    expect(returned).toEqual(item)
    expect(result.current.uploading).toBe(false)
    expect(result.current.error).toBeNull()
    expect(result.current.progress).toBe(1)
  })

  it('forwards an onProgress callback to the client and settles progress at 1 on success', async () => {
    const seen: number[] = []
    const { result } = renderHook(() => useUpload(clientProgress(seen)))
    await act(async () => { await result.current.upload(file) })
    // The hook supplied a real onProgress (not undefined) — proves the progress wiring is forwarded…
    expect(seen).toEqual([0.5])
    // …and the success path drives progress to 1.
    expect(result.current.progress).toBe(1)
  })

  it('surfaces a typed rejection in error and rethrows', async () => {
    const { result } = renderHook(() => useUpload(clientReject()))
    await act(async () => {
      await expect(result.current.upload(file)).rejects.toBeInstanceOf(UploadRejectedError)
    })
    await waitFor(() => expect(result.current.error).toBeInstanceOf(UploadRejectedError))
  })

  it('reset clears error', async () => {
    const { result } = renderHook(() => useUpload(clientReject()))
    await act(async () => { await result.current.upload(file).catch(() => {}) })
    await waitFor(() => expect(result.current.error).not.toBeNull())
    act(() => result.current.reset())
    expect(result.current.error).toBeNull()
  })

  it('rejects a re-entrant upload while one is in flight, without clobbering the first', async () => {
    let release!: (v: MediaItem) => void
    const gated: MediaClient = {
      upload: () => new Promise<MediaItem>((res) => { release = res }),
      list: async () => ({ items: [] }),
      remove: async () => {},
    }
    const { result } = renderHook(() => useUpload(gated))
    let first!: Promise<MediaItem>
    act(() => { first = result.current.upload(file) })
    await waitFor(() => expect(result.current.uploading).toBe(true))
    // second call while the first is unresolved → rejects with the TYPED error, no second upload
    await act(async () => {
      const err = await result.current.upload(file).catch((e: unknown) => e)
      expect(isUploadInFlightError(err)).toBe(true)
    })
    // first upload still resolves cleanly
    await act(async () => { release(item); await first })
    expect(result.current.error).toBeNull()
    expect(result.current.uploading).toBe(false)
  })
})
