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()
  })
})
