import { describe, expect, it, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { renderToStaticMarkup } from 'react-dom/server'
import { QueryBoundary, type QueryLike } from './QueryBoundary'

function query(overrides: Partial<QueryLike<string>> = {}): QueryLike<string> {
  return {
    data: undefined,
    isPending: false,
    isError: false,
    refetch: () => {},
    ...overrides,
  }
}

describe('QueryBoundary', () => {
  it('shows the skeleton (guarded) when there is no data and the query is pending', () => {
    vi.useFakeTimers()
    render(
      <QueryBoundary query={query({ isPending: true })} skeleton={<div>Loading rows</div>}>
        {(data) => <div>{data}</div>}
      </QueryBoundary>,
    )

    expect(screen.queryByText('Loading rows')).toBeNull()
    act(() => {
      vi.advanceTimersByTime(200)
    })
    expect(screen.getByText('Loading rows')).toBeTruthy()
    vi.useRealTimers()
  })

  it('renders the default errorFallback with a working retry when there is no data and the query errors', () => {
    const retry = vi.fn()

    render(
      <QueryBoundary query={query({ isError: true, error: new Error('boom'), refetch: retry })} skeleton={null}>
        {(data) => <div>{data}</div>}
      </QueryBoundary>,
    )

    expect(screen.getByRole('alert')).toHaveTextContent('boom')
    fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
    expect(retry).toHaveBeenCalledTimes(1)
  })

  it('moves focus to the error region on error entry', () => {
    render(
      <QueryBoundary query={query({ isError: true, error: new Error('boom') })} skeleton={null}>
        {(data) => <div>{data}</div>}
      </QueryBoundary>,
    )

    expect(screen.getByRole('alert')).toHaveFocus()
  })

  it('renders data and mounts RefreshSignal while a background refetch of fresh data is in flight', () => {
    render(
      <QueryBoundary query={query({ data: 'Settled rows', isFetching: true, isStale: true })} skeleton={null}>
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )

    expect(screen.getByText('Settled rows')).toBeTruthy()
    expect(screen.getByTestId('refresh-signal')).toHaveAttribute('data-variant', 'refreshing')
  })

  it('marks the boundary aria-busy while fetching', () => {
    const { container } = render(
      <QueryBoundary query={query({ data: 'Settled rows', isFetching: true, isStale: true })} skeleton={null}>
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )
    expect(container.querySelector('[aria-busy="true"]')).toBeTruthy()
  })

  it('shows the offline affordance instead of a live-refresh hint when the query is paused', () => {
    render(
      <QueryBoundary
        query={query({ data: 'Settled rows', isFetching: true, isStale: true, isPaused: true })}
        skeleton={null}
      >
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )

    expect(screen.getByText('Showing cached data')).toBeTruthy()
    expect(screen.queryByTestId('refresh-signal')).toBeNull()
  })

  it('4th state — keeps stale data visible AND surfaces the failing-refetch affordance, never swallowing it', () => {
    const retry = vi.fn()
    render(
      <QueryBoundary
        query={query({ data: 'Settled rows', isError: true, error: new Error('offline'), refetch: retry })}
        skeleton={null}
      >
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )

    expect(screen.getByText('Settled rows')).toBeTruthy()
    expect(screen.getByTestId('refresh-signal')).toHaveAttribute('data-variant', 'error')
    expect(screen.getByText('offline')).toBeTruthy()
    expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy()
  })

  it('falls back to isFetching+data when isStale is absent (non-TanStack hook), so the SWR hint is not dead', () => {
    render(
      <QueryBoundary query={query({ data: 'Settled rows', isFetching: true })} skeleton={null}>
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )
    expect(screen.getByTestId('refresh-signal')).toBeTruthy()
  })

  it('announces "updated" via the polite live region on a fetch to settle transition', async () => {
    const { rerender } = render(
      <QueryBoundary query={query({ data: 'Settled rows', isFetching: true, isStale: true })} skeleton={null}>
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )

    rerender(
      <QueryBoundary query={query({ data: 'Settled rows', isFetching: false, isStale: false })} skeleton={null}>
        {(data) => <span>{data}</span>}
      </QueryBoundary>,
    )

    expect(await screen.findByText('updated')).toBeTruthy()
  })

  it('SSR: gates cache-derived content behind mount so the server render never disagrees with cached-client state', () => {
    const html = renderToStaticMarkup(
      <QueryBoundary query={query({ data: 'Cached rows' })} skeleton={<div>Loading rows</div>}>
        {(data) => <div>{data}</div>}
      </QueryBoundary>,
    )

    expect(html).not.toContain('Cached rows')
  })
})
