import { StrictMode } from 'react'
import { act, renderHook, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useAsyncResource } from './useAsyncResource.js'

describe('useAsyncResource', () => {
  it('fetches on mount when unseeded', async () => {
    const fetcher = vi.fn().mockResolvedValue('A')
    const { result } = renderHook(() => useAsyncResource(fetcher, 'k1', undefined))
    expect(result.current.loading).toBe(true)
    await waitFor(() => expect(result.current.loading).toBe(false))
    expect(result.current.data).toBe('A')
    expect(fetcher).toHaveBeenCalledTimes(1)
  })

  it('seeded: loading false, data from seed, NO mount fetch', async () => {
    const fetcher = vi.fn().mockResolvedValue('FETCHED')
    const { result } = renderHook(() =>
      useAsyncResource(fetcher, 'k1', { initialData: 'SEED' }),
    )
    expect(result.current.loading).toBe(false)
    expect(result.current.data).toBe('SEED')
    await waitFor(() => expect(fetcher).not.toHaveBeenCalled())
  })

  it('seeded: NO mount fetch even under React StrictMode (dev double-invoke)', async () => {
    const fetcher = vi.fn().mockResolvedValue('FETCHED')
    const { result } = renderHook(
      () => useAsyncResource(fetcher, 'k1', { initialData: 'SEED' }),
      { wrapper: ({ children }) => <StrictMode>{children}</StrictMode> },
    )
    expect(result.current.loading).toBe(false)
    expect(result.current.data).toBe('SEED')
    // StrictMode setup→cleanup→setup must NOT fetch for the seeded key — a
    // one-shot boolean flag would break here (flipped on the first setup).
    await waitFor(() => expect(fetcher).not.toHaveBeenCalled())
  })

  it('seeded with null is still a seed (no fetch) — SSR "not found"', async () => {
    const fetcher = vi.fn().mockResolvedValue('X')
    const { result } = renderHook(() =>
      useAsyncResource<string | null>(fetcher, 'k1', { initialData: null }),
    )
    expect(result.current.loading).toBe(false)
    expect(result.current.data).toBeNull()
    await waitFor(() => expect(fetcher).not.toHaveBeenCalled())
  })

  it('reload() refetches via the client', async () => {
    const fetcher = vi.fn().mockResolvedValueOnce('A').mockResolvedValueOnce('B')
    const { result } = renderHook(() => useAsyncResource(fetcher, 'k1', undefined))
    await waitFor(() => expect(result.current.data).toBe('A'))
    act(() => result.current.reload())
    await waitFor(() => expect(result.current.data).toBe('B'))
    expect(fetcher).toHaveBeenCalledTimes(2)
  })

  it('seeded then key-change refetches (interactivity rides on the seed)', async () => {
    const fetcher = vi.fn().mockResolvedValue('FETCHED')
    let key = 'k1'
    const { result, rerender } = renderHook(() =>
      useAsyncResource(fetcher, key, { initialData: 'SEED' }),
    )
    expect(result.current.data).toBe('SEED')
    expect(fetcher).not.toHaveBeenCalled()
    key = 'k2'
    rerender()
    await waitFor(() => expect(result.current.data).toBe('FETCHED'))
    expect(fetcher).toHaveBeenCalledTimes(1)
  })

  it('captures a rejected fetch into error state, not a throw', async () => {
    const fetcher = vi.fn().mockRejectedValue(new Error('boom'))
    const { result } = renderHook(() => useAsyncResource(fetcher, 'k1', undefined))
    await waitFor(() => expect(result.current.error).toBeInstanceOf(Error))
    expect(result.current.error?.message).toBe('boom')
    expect(result.current.loading).toBe(false)
  })

  it('does not commit state after unmount (cancel flag)', async () => {
    let resolve!: (v: string) => void
    const fetcher = vi.fn().mockReturnValue(new Promise<string>((r) => (resolve = r)))
    const { result, unmount } = renderHook(() => useAsyncResource(fetcher, 'k1', undefined))
    unmount()
    act(() => resolve('LATE'))
    // No act-warning / state update after unmount; data stays null.
    expect(result.current.data).toBeNull()
  })

  it('ignores a stale in-flight resolution after key change (last-write-wins)', async () => {
    let resolveFirst: (v: string) => void = () => {}
    const fetcher = vi
      .fn<() => Promise<string>>()
      .mockImplementationOnce(() => new Promise<string>((r) => { resolveFirst = r }))
      .mockImplementationOnce(() => Promise.resolve('second'))
    const { result, rerender } = renderHook(
      ({ key }: { key: string }) => useAsyncResource<string>(fetcher, key, undefined),
      { initialProps: { key: 'a' } },
    )
    rerender({ key: 'b' }) // supersedes the in-flight 'a' fetch
    await waitFor(() => expect(result.current.data).toBe('second'))
    resolveFirst('first') // late resolution of the cancelled fetch
    await Promise.resolve()
    expect(result.current.data).toBe('second') // stale 'first' never clobbers
  })
})
