import { act, renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
  idleSlot,
  isStaleResponse,
  selectedSlotCleared,
  selectedSlotRequested,
  slotFailed,
  slotRequested,
  slotSucceeded,
  useReadResource,
} from './readResource'

afterEach(() => {
  vi.useRealTimers()
})

async function settle() {
  await act(async () => { await Promise.resolve() })
}

function deferred<T>() {
  let resolve!: (value: T) => void
  const promise = new Promise<T>((resolvePromise) => { resolve = resolvePromise })
  return { promise, resolve }
}

describe('slot phase machine', () => {
  const ready = { ...slotSucceeded(['a']), total: 1 }

  it('silent refresh over ready data keeps the snapshot visible', () => {
    expect(slotRequested(ready, true).phase).toBe('refreshing')
    expect(slotRequested(ready, true).value).toEqual(['a'])
    expect(slotRequested(idleSlot<string[]>([]), true).phase).toBe('loading')
  })

  it('failure retains a non-empty snapshot and surfaces errors otherwise', () => {
    expect(slotFailed(ready, '网络错误')).toEqual({ ...ready, phase: 'ready', error: null })
    expect(slotFailed(idleSlot<string[]>([]), '网络错误').phase).toBe('error')
    expect(slotFailed(idleSlot<string | null>(null), '不存在').error).toBe('不存在')
  })
})

describe('selected slot race guard', () => {
  it('resets the value when the selection changes and drops stale responses', () => {
    const first = selectedSlotRequested(selectedSlotCleared<object>(0), 'a', 1, false)
    const second = selectedSlotRequested(first, 'b', 2, false)
    expect(second.value).toBeNull()
    expect(second.selectedId).toBe('b')
    expect(isStaleResponse(second, 'a', 1)).toBe(true)
    expect(isStaleResponse(second, 'b', 2)).toBe(false)
  })
})

describe('useReadResource', () => {
  it('loads on mount, keeps the snapshot on silent failure, and polls while gated on', async () => {
    vi.useFakeTimers()
    const fetcher = vi.fn()
      .mockResolvedValueOnce({ ok: true })
      .mockRejectedValueOnce(new Error('网络错误'))
    const { result } = renderHook(() => useReadResource(fetcher, { pollMs: 5000 }))
    await settle()
    expect(result.current.slot).toEqual({ phase: 'ready', value: { ok: true }, error: null })

    await act(async () => { await vi.advanceTimersByTimeAsync(5000) })
    await settle()
    expect(fetcher).toHaveBeenCalledTimes(2)
    expect(result.current.slot.phase).toBe('ready')
    expect(result.current.slot.value).toEqual({ ok: true })
  })

  it('stops polling when the gate flips false', async () => {
    vi.useFakeTimers()
    const fetcher = vi.fn().mockResolvedValue('x')
    const { rerender } = renderHook(
      ({ polling }) => useReadResource(fetcher, { pollMs: 5000, polling }),
      { initialProps: { polling: true } },
    )
    await settle()
    expect(fetcher).toHaveBeenCalledTimes(1)

    rerender({ polling: false })
    await act(async () => { await vi.advanceTimersByTimeAsync(15000) })
    expect(fetcher).toHaveBeenCalledTimes(1)
  })

  it('drops a stale response after the key changes', async () => {
    const first = deferred<string>()
    const second = deferred<string>()
    const fetchers: Record<string, () => Promise<string>> = {
      a: () => first.promise,
      b: () => second.promise,
    }
    const { result, rerender } = renderHook(
      ({ key }) => useReadResource(() => fetchers[key](), { key }),
      { initialProps: { key: 'a' } },
    )
    rerender({ key: 'b' })
    await act(async () => { second.resolve('B') })
    expect(result.current.slot.value).toBe('B')

    await act(async () => { first.resolve('A') })
    expect(result.current.slot.value).toBe('B')
  })

  it('clears the slot when the key becomes null', async () => {
    const fetcher = vi.fn().mockResolvedValue('x')
    const { result, rerender } = renderHook(
      ({ key }) => useReadResource(fetcher, { key }),
      { initialProps: { key: 'a' as string | null } },
    )
    await settle()
    expect(result.current.slot.phase).toBe('ready')

    rerender({ key: null })
    expect(result.current.slot).toEqual({ phase: 'idle', value: null, error: null })
  })
})
