// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, renderHook, waitFor } from '@testing-library/react'

// `cmsApi` is the hook's only I/O dependency — mock it so retry/backoff
// behaviour can be exercised deterministically. Responses follow the SDK
// `{ data, error, status }` envelope.
const cmsApiMock = vi.fn()
vi.mock('../../lib/api.js', () => ({
  cmsApi: (...args: unknown[]) => cmsApiMock(...args),
}))

const { useApiData, __clearApiDataCache } = await import('../../lib/useApiData.js')

beforeEach(() => {
  cmsApiMock.mockReset()
  __clearApiDataCache()
})

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

describe('useApiData', () => {
  it('loads data on mount and clears the loading flag', async () => {
    cmsApiMock.mockResolvedValue({ data: { ok: 1 }, status: 200 })

    const { result } = renderHook(() => useApiData<{ ok: number }>('/x'))

    expect(result.current.loading).toBe(true)
    await waitFor(() => expect(result.current.loading).toBe(false))

    expect(result.current.data).toEqual({ ok: 1 })
    expect(result.current.error).toBeNull()
    expect(result.current.exhausted).toBe(false)
    expect(cmsApiMock).toHaveBeenCalledTimes(1)
  })

  it('does not fetch when the endpoint is null', () => {
    const { result } = renderHook(() => useApiData<unknown>(null))

    expect(result.current.loading).toBe(false)
    expect(cmsApiMock).not.toHaveBeenCalled()
  })

  it('retries with exponential backoff, then marks the request exhausted', async () => {
    vi.useFakeTimers()
    cmsApiMock.mockResolvedValue({ error: 'boom', status: 500 })

    const { result } = renderHook(() => useApiData('/x', null, { retryBaseMs: 10 }))

    // Flush the initial attempt.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0)
    })
    expect(cmsApiMock).toHaveBeenCalledTimes(1)
    expect(result.current.exhausted).toBe(false)

    // Backoff schedule with base 10ms: 10, 20, 40.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(10)
    })
    await act(async () => {
      await vi.advanceTimersByTimeAsync(20)
    })
    await act(async () => {
      await vi.advanceTimersByTimeAsync(40)
    })

    // 1 initial + 3 retries = 4 total, then it gives up.
    expect(cmsApiMock).toHaveBeenCalledTimes(4)
    expect(result.current.exhausted).toBe(true)
    expect(result.current.error).toBe('boom')
    expect(result.current.loading).toBe(false)
  })

  it('recovers via refetch after exhausting retries', async () => {
    vi.useFakeTimers()
    cmsApiMock.mockResolvedValue({ error: 'boom', status: 500 })

    const { result } = renderHook(() =>
      useApiData<{ ok: boolean }>('/x', null, { retryBaseMs: 10 }),
    )

    await act(async () => {
      await vi.advanceTimersByTimeAsync(0)
    })
    // Run out the full backoff schedule (10 + 20 + 40).
    await act(async () => {
      await vi.advanceTimersByTimeAsync(70)
    })
    expect(result.current.exhausted).toBe(true)

    cmsApiMock.mockResolvedValue({ data: { ok: true }, status: 200 })
    await act(async () => {
      result.current.refetch()
    })
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0)
    })

    expect(result.current.data).toEqual({ ok: true })
    expect(result.current.exhausted).toBe(false)
    expect(result.current.error).toBeNull()
  })

  it('serves a warm cache on remount without refetching when cacheMs is set', async () => {
    cmsApiMock.mockResolvedValue({ data: { v: 1 }, status: 200 })

    const first = renderHook(() => useApiData<{ v: number }>('/cached', null, { cacheMs: 10_000 }))
    await waitFor(() => expect(first.result.current.loading).toBe(false))
    expect(first.result.current.data).toEqual({ v: 1 })
    expect(cmsApiMock).toHaveBeenCalledTimes(1)
    first.unmount()

    // A fresh mount within the TTL reads the cache synchronously — data is
    // present immediately, loading never flips true, and no new request fires.
    const second = renderHook(() => useApiData<{ v: number }>('/cached', null, { cacheMs: 10_000 }))
    expect(second.result.current.data).toEqual({ v: 1 })
    expect(second.result.current.loading).toBe(false)
    expect(cmsApiMock).toHaveBeenCalledTimes(1)
  })

  it('refetch bypasses the cache and updates it', async () => {
    cmsApiMock.mockResolvedValue({ data: { v: 1 }, status: 200 })

    const { result } = renderHook(() =>
      useApiData<{ v: number }>('/cached2', null, { cacheMs: 10_000 }),
    )
    await waitFor(() => expect(result.current.loading).toBe(false))
    expect(cmsApiMock).toHaveBeenCalledTimes(1)

    cmsApiMock.mockResolvedValue({ data: { v: 2 }, status: 200 })
    await act(async () => {
      result.current.refetch()
    })
    await waitFor(() => expect(result.current.data).toEqual({ v: 2 }))
    expect(cmsApiMock).toHaveBeenCalledTimes(2)
  })
})
