import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { createElement, type ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { collectorQueryKeys, COLLECTOR_POLL_MS, useCollectorState, useHookControls, useIncidents, useSetHookControl } from './collector-queries'
import { CollectorHttpError, fetchIncidents } from './collector-client'

const staleResponse = {
  controls: { version: 'hook-controls/v1' as const, hooks: { 'background-jobs-blocker': true } },
  issue: null,
}

const postResponse = {
  controls: { version: 'hook-controls/v1' as const, hooks: { 'background-jobs-blocker': false } },
  issue: null,
}

let fetchMode: 'initial' | 'late' = 'initial'

vi.mock('./collector-client', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./collector-client')>()
  return {
    ...actual,
    fetchCollectorState: vi.fn(async () => ({ panels: null, adapters: null })),
    fetchIncidents: vi.fn(),
    fetchHookControls: vi.fn(({ signal }: { signal?: AbortSignal } = {}) => {
      if (fetchMode === 'initial') return Promise.resolve(staleResponse)
      return new Promise((_resolve, reject) => {
        if (signal?.aborted) reject(new DOMException('Aborted', 'AbortError'))
        signal?.addEventListener('abort', () => {
          reject(new DOMException('Aborted', 'AbortError'))
        }, { once: true })
      })
    }),
    setHookControl: vi.fn(async () => postResponse),
    repairHookControls: vi.fn(),
  }
})

function wrapper(client: QueryClient) {
  return ({ children }: { children: ReactNode }) => createElement(QueryClientProvider, { client }, children)
}

describe('incident polling', () => {
  afterEach(() => { vi.useRealTimers(); vi.clearAllMocks() })

  it('makes one bounded first-page request per tick even with multiple cached pages', async () => {
    vi.useFakeTimers()
    const response = { incidents: [], coverage: { stale: false }, page: { highWater: 10, nextCursor: null, exhausted: true, order: 'id_desc' as const } }
    vi.mocked(fetchIncidents).mockResolvedValue(response)
    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
    const { result } = renderHook(() => useIncidents({ scope: 'all' }), { wrapper: wrapper(client) })
    await vi.advanceTimersByTimeAsync(0)
    expect(result.current.isSuccess).toBe(true)
    vi.mocked(fetchIncidents).mockClear()
    await vi.advanceTimersByTimeAsync(COLLECTOR_POLL_MS)
    expect(fetchIncidents).toHaveBeenCalledTimes(1)
    expect(vi.mocked(fetchIncidents).mock.calls[0]?.[0]).toEqual({ scope: 'all' })
    expect(vi.mocked(fetchIncidents).mock.calls[0]?.[1]).toBeInstanceOf(AbortSignal)
  })

  it('lets only the polling timer resume once at the 429 boundary and restores cadence', async () => {
    vi.useFakeTimers()
    vi.mocked(fetchIncidents).mockRejectedValue(new CollectorHttpError(429, null, 'limited', 1_000))
    const client = new QueryClient()
    renderHook(() => useIncidents({ scope: 'all' }), { wrapper: wrapper(client) })
    await vi.advanceTimersByTimeAsync(0)
    expect(fetchIncidents).toHaveBeenCalledTimes(1)
    await vi.advanceTimersByTimeAsync(COLLECTOR_POLL_MS - 1)
    expect(fetchIncidents).toHaveBeenCalledTimes(1)
    await vi.advanceTimersByTimeAsync(1)
    expect(fetchIncidents).toHaveBeenCalledTimes(2)
    await vi.advanceTimersByTimeAsync(COLLECTOR_POLL_MS - 1)
    expect(fetchIncidents).toHaveBeenCalledTimes(2)
    await vi.advanceTimersByTimeAsync(1)
    expect(fetchIncidents).toHaveBeenCalledTimes(3)
  })
})

describe('collector hook-controls queries', () => {
  afterEach(() => {
    fetchMode = 'initial'
    vi.clearAllMocks()
  })

  it('normalizes malformed persisted collector state before consumers read it', async () => {
    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
    client.setQueryData(collectorQueryKeys.state, { panels: null, adapters: null })
    const { result } = renderHook(() => useCollectorState(), { wrapper: wrapper(client) })

    await waitFor(() => expect(result.current.data).toEqual({ panels: [], adapters: [] }))
  })

  it('cancels focus/reconnect GET again before applying mutation response', async () => {
    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
    const cancelSpy = vi.spyOn(client, 'cancelQueries')
    const setSpy = vi.spyOn(client, 'setQueryData')
    const { result: controls } = renderHook(() => useHookControls(), { wrapper: wrapper(client) })
    const { result: mutation } = renderHook(() => useSetHookControl(), { wrapper: wrapper(client) })

    await waitFor(() => expect(controls.current.data).toEqual(staleResponse))

    mutation.current.mutate({ id: 'background-jobs-blocker', enabled: false })
    await waitFor(() => expect(cancelSpy).toHaveBeenCalledTimes(1))

    fetchMode = 'late'
    void controls.current.refetch()
    await waitFor(() => expect(controls.current.isFetching).toBe(true))

    await waitFor(() => {
      expect(mutation.current.isSuccess).toBe(true)
      expect(cancelSpy).toHaveBeenCalledTimes(2)
      expect(client.getQueryData(collectorQueryKeys.hookControls)).toEqual(postResponse)
    })
    expect(cancelSpy.mock.invocationCallOrder[1]).toBeLessThan(setSpy.mock.invocationCallOrder.at(-1)!)
  })
})
