import { act, renderHook, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { ReviewsProvider } from './ReviewsProvider.js'
import { useSubmitReview } from './useSubmitReview.js'
import { isNotPurchasedError } from './errors.js'
import type { ReviewsClient } from './client.js'

const PURCHASE_ID = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'

function deferred<T>() {
  let resolve!: (v: T) => void
  let reject!: (e: unknown) => void
  const promise = new Promise<T>((res, rej) => {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}

const wireReview = (id: string, body: string) => ({
  id,
  productId: 'p1',
  userId: 'u1',
  vendorId: null,
  rating: 5,
  body,
  status: 'approved' as const,
  vendorReply: null,
  createdAt: '2026-06-25T10:00:00.000Z',
  updatedAt: '2026-06-25T10:00:00.000Z',
})

function mk(submitReview: ReviewsClient['submitReview']) {
  const client: ReviewsClient = {
    listReviews: async () => ({ items: [], nextCursor: null }),
    getRatingAggregate: async () => ({
      productId: 'p1',
      avg: 0,
      count: 0,
      distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
    }),
    submitReview,
  }
  const wrapper = ({ children }: { children: ReactNode }) => (
    <ReviewsProvider client={client}>{children}</ReviewsProvider>
  )
  return { wrapper }
}

describe('useSubmitReview', () => {
  it('sets optimistic placeholder synchronously at call-time (id:null, status:pending)', async () => {
    const d = deferred<ReturnType<typeof wireReview>>()
    const submitReview = vi.fn().mockReturnValue(d.promise)
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    let submitPromise!: Promise<unknown>
    act(() => {
      submitPromise = result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 4, body: 'nice' })
    })

    expect(result.current.optimistic).toMatchObject({
      id: null,
      productId: 'p1',
      rating: 4,
      body: 'nice',
      status: 'pending',
      vendorReply: null,
      vendorId: null,
    })
    expect(result.current.optimistic?.id).toBeNull()
    expect(result.current.pending).toBe(true)

    await act(async () => {
      d.resolve(wireReview('r1', 'nice'))
      await submitPromise
    })
  })

  it('resolve sets revived authoritative result', async () => {
    const submitReview = vi.fn().mockResolvedValue(wireReview('r9', 'authoritative'))
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    await act(async () => {
      await result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 5 })
    })

    expect(result.current.result?.id).toBe('r9')
    expect(result.current.result?.body).toBe('authoritative')
    expect(result.current.result?.createdAt).toBeInstanceOf(Date)
    expect(result.current.error).toBeUndefined()
  })

  it('reject clears optimistic and sets structural error', async () => {
    const err = Object.assign(new Error('not purchased'), { name: 'NotPurchasedError', code: 'NOT_PURCHASED' })
    const submitReview = vi.fn().mockRejectedValue(err)
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    await act(async () => {
      await expect(result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 3 })).rejects.toBe(err)
    })

    expect(result.current.optimistic).toBeUndefined()
    expect(isNotPurchasedError(result.current.error)).toBe(true)
  })

  it('queued path: last-write-wins when two submits fire same-tick with slow first resolve', async () => {
    const first = deferred<ReturnType<typeof wireReview>>()
    const second = deferred<ReturnType<typeof wireReview>>()
    const submitReview = vi.fn()
      .mockReturnValueOnce(first.promise)
      .mockReturnValueOnce(second.promise)
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    let p1!: Promise<unknown>
    let p2!: Promise<unknown>
    act(() => {
      p1 = result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 1, body: 'first' })
      p2 = result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 5, body: 'second' })
    })

    expect(result.current.pending).toBe(true)
    expect(result.current.optimistic?.body).toBe('second')
    expect(result.current.optimistic?.rating).toBe(5)

    await act(async () => {
      second.resolve(wireReview('r-second', 'second'))
      await p2
    })

    await waitFor(() => expect(result.current.result?.body).toBe('second'))
    expect(result.current.result?.id).toBe('r-second')

    await act(async () => {
      first.resolve(wireReview('r-first', 'first'))
      await p1
    })

    expect(result.current.result?.body).toBe('second')
    expect(result.current.result?.id).toBe('r-second')

    await waitFor(() => expect(result.current.pending).toBe(false))
  })

  it('reset mid-flight orphans the in-flight resolve and keeps pending honest', async () => {
    const d = deferred<ReturnType<typeof wireReview>>()
    const submitReview = vi.fn().mockReturnValue(d.promise)
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    let p!: Promise<unknown>
    act(() => {
      p = result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 4, body: 'first' })
    })
    expect(result.current.pending).toBe(true)
    expect(result.current.optimistic?.body).toBe('first')

    act(() => result.current.reset())
    expect(result.current.optimistic).toBeUndefined()

    // The orphaned resolve must NOT land result, and pending must settle to
    // false (not underflow): a second submit then still flips pending true.
    await act(async () => {
      d.resolve(wireReview('r-orphan', 'first'))
      await p
    })
    expect(result.current.result).toBeUndefined()
    await waitFor(() => expect(result.current.pending).toBe(false))

    const d2 = deferred<ReturnType<typeof wireReview>>()
    submitReview.mockReturnValueOnce(d2.promise)
    act(() => {
      void result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 5, body: 'second' })
    })
    expect(result.current.pending).toBe(true)
    await act(async () => {
      d2.resolve(wireReview('r2', 'second'))
    })
    await waitFor(() => expect(result.current.pending).toBe(false))
  })

  it('reset clears all state', async () => {
    const submitReview = vi.fn().mockResolvedValue(wireReview('r1', 'x'))
    const { wrapper } = mk(submitReview)
    const { result } = renderHook(() => useSubmitReview(), { wrapper })

    await act(async () => {
      await result.current.submit({ productId: 'p1', purchaseId: PURCHASE_ID, rating: 5 })
    })
    act(() => result.current.reset())
    expect(result.current.result).toBeUndefined()
    expect(result.current.optimistic).toBeUndefined()
    expect(result.current.error).toBeUndefined()
    expect(result.current.pending).toBe(false)
  })
})
