import { act, renderHook, waitFor } from '@testing-library/react'
import type { ReactNode } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { DEFAULT_COMMISSION_BPS } from '@platform-modules/commerce-marketplace'
import { MarketplaceProvider } from './MarketplaceProvider.js'
import { useApplyAsVendor } from './useApplyAsVendor.js'
import type { MarketplaceClient } from './client.js'

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 wireVendor = (over: Record<string, unknown> = {}) => ({
  id: 'v1',
  ownerUserId: 'u1',
  name: 'Acme',
  status: 'pending',
  commissionBps: DEFAULT_COMMISSION_BPS,
  createdAt: '2026-06-01T12:00:00.000Z',
  updatedAt: '2026-06-02T12:00:00.000Z',
  ...over,
})

function wrap(client: MarketplaceClient) {
  return ({ children }: { children: ReactNode }) => (
    <MarketplaceProvider client={client}>{children}</MarketplaceProvider>
  )
}

function baseClient(overrides: Partial<MarketplaceClient> = {}): MarketplaceClient {
  return {
    getVendorProfile: vi.fn(),
    getVendorOrders: vi.fn(),
    applyAsVendor: vi.fn(),
    createVendorProduct: vi.fn(),
    updateVendorProduct: vi.fn(),
    ...overrides,
  }
}

describe('useApplyAsVendor', () => {
  it('sets optimistic placeholder synchronously at call-time', async () => {
    const d = deferred<ReturnType<typeof wireVendor>>()
    const applyAsVendor = vi.fn().mockReturnValue(d.promise)
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    let applyPromise!: Promise<unknown>
    act(() => {
      applyPromise = result.current.apply({ name: 'Acme' })
    })

    expect(result.current.optimistic).toEqual({
      id: null,
      ownerUserId: null,
      name: 'Acme',
      status: 'pending',
      commissionBps: DEFAULT_COMMISSION_BPS,
      createdAt: null,
      updatedAt: null,
    })
    expect(result.current.pending).toBe(true)

    await act(async () => {
      d.resolve(wireVendor({ name: 'Acme' }))
      await applyPromise
    })
  })

  it('resolve sets revived result and clears optimistic', async () => {
    const applyAsVendor = vi.fn().mockResolvedValue(wireVendor({ id: 'v9', name: 'Vendor Nine' }))
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    await act(async () => {
      await result.current.apply({ name: 'Vendor Nine' })
    })

    expect(result.current.result?.id).toBe('v9')
    expect(result.current.result?.name).toBe('Vendor Nine')
    expect(result.current.result?.createdAt).toBeInstanceOf(Date)
    expect(result.current.optimistic).toBeUndefined()
    expect(result.current.error).toBeUndefined()
  })

  it('reject clears optimistic and sets error', async () => {
    const err = new Error('already exists')
    const applyAsVendor = vi.fn().mockRejectedValue(err)
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    await act(async () => {
      await expect(result.current.apply({ name: 'Dup' })).rejects.toThrow('already exists')
    })

    expect(result.current.optimistic).toBeUndefined()
    expect(result.current.error).toBeInstanceOf(Error)
    expect(result.current.error?.message).toBe('already exists')
  })

  it('pending is true while in flight', async () => {
    const d = deferred<ReturnType<typeof wireVendor>>()
    const applyAsVendor = vi.fn().mockReturnValue(d.promise)
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    let applyPromise!: Promise<unknown>
    act(() => {
      applyPromise = result.current.apply({ name: 'Pending Co' })
    })

    expect(result.current.pending).toBe(true)

    await act(async () => {
      d.resolve(wireVendor({ name: 'Pending Co' }))
      await applyPromise
    })

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

  it('reset clears result, error, and optimistic', async () => {
    const applyAsVendor = vi.fn().mockResolvedValue(wireVendor())
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    await act(async () => {
      await result.current.apply({ name: 'Acme' })
    })

    act(() => result.current.reset())

    expect(result.current.result).toBeUndefined()
    expect(result.current.error).toBeUndefined()
    expect(result.current.optimistic).toBeUndefined()
  })

  it('queued path: last-write-wins when two applies fire same-tick with slow first resolve', async () => {
    const first = deferred<ReturnType<typeof wireVendor>>()
    const second = deferred<ReturnType<typeof wireVendor>>()
    const applyAsVendor = vi.fn().mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise)
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    let p1!: Promise<unknown>
    let p2!: Promise<unknown>
    act(() => {
      p1 = result.current.apply({ name: 'First Shop' })
      p2 = result.current.apply({ name: 'Second Shop' })
    })

    expect(result.current.pending).toBe(true)
    expect(result.current.optimistic?.name).toBe('Second Shop')

    await act(async () => {
      second.resolve(wireVendor({ id: 'v-second', name: 'Second Shop' }))
      await p2
    })

    await waitFor(() => expect(result.current.result?.name).toBe('Second Shop'))
    expect(result.current.result?.id).toBe('v-second')

    await act(async () => {
      first.resolve(wireVendor({ id: 'v-first', name: 'First Shop' }))
      await p1
    })

    expect(result.current.result?.name).toBe('Second Shop')
    expect(result.current.result?.id).toBe('v-second')

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

  it('resolve clears stale error from prior rejection', async () => {
    const applyAsVendor = vi
      .fn()
      .mockRejectedValueOnce(new Error('already exists'))
      .mockResolvedValueOnce(wireVendor())
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    await act(async () => {
      await expect(result.current.apply({ name: 'Dup' })).rejects.toThrow('already exists')
    })
    expect(result.current.error).toBeInstanceOf(Error)

    await act(async () => {
      await result.current.apply({ name: 'Acme' })
    })
    expect(result.current.error).toBeUndefined()
    expect(result.current.result?.id).toBe('v1')
  })

  it('reject clears stale result from prior success', async () => {
    const applyAsVendor = vi
      .fn()
      .mockResolvedValueOnce(wireVendor({ id: 'v1' }))
      .mockRejectedValueOnce(new Error('fail'))
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    await act(async () => {
      await result.current.apply({ name: 'Acme' })
    })
    expect(result.current.result?.id).toBe('v1')

    await act(async () => {
      await expect(result.current.apply({ name: 'Acme' })).rejects.toThrow('fail')
    })
    expect(result.current.result).toBeUndefined()
    expect(result.current.error).toBeInstanceOf(Error)
    expect(result.current.error?.message).toBe('fail')
  })

  it('reset() while in-flight orphans the late resolve', async () => {
    const d = deferred<ReturnType<typeof wireVendor>>()
    const applyAsVendor = vi.fn().mockReturnValue(d.promise)
    const client = baseClient({ applyAsVendor })
    const { result } = renderHook(() => useApplyAsVendor(), { wrapper: wrap(client) })

    let applyPromise!: Promise<unknown>
    act(() => {
      applyPromise = result.current.apply({ name: 'Acme' })
    })

    act(() => result.current.reset())

    await act(async () => {
      d.resolve(wireVendor())
      await applyPromise
    })

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