import { waitFor } from '@testing-library/dom'
import {
  afterEach,
  beforeEach,
  describe,
  expect,
  expectTypeOf,
  test,
  vi,
} from 'vitest'
import { QueryObserver, focusManager } from '..'
import { pendingThenable } from '../thenable'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient, QueryObserverResult } from '..'

describe('queryObserver', () => {
  let queryClient: QueryClient

  beforeEach(() => {
    queryClient = createQueryClient({
      defaultOptions: {
        queries: {
          experimental_prefetchInRender: true,
        },
      },
    })
    queryClient.mount()
  })

  afterEach(() => {
    queryClient.clear()
  })

  test('should trigger a fetch when subscribed', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const observer = new QueryObserver(queryClient, { queryKey: key, queryFn })
    const unsubscribe = observer.subscribe(() => undefined)
    await sleep(1)
    unsubscribe()
    expect(queryFn).toHaveBeenCalledTimes(1)
  })

  test('should be able to read latest data after subscribing', async () => {
    const key = queryKey()
    queryClient.setQueryData(key, 'data')
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
    })

    const unsubscribe = observer.subscribe(vi.fn())

    expect(observer.getCurrentResult()).toMatchObject({
      status: 'success',
      data: 'data',
    })

    unsubscribe()
  })

  describe('enabled is a callback that initially returns false', () => {
    let observer: QueryObserver<string, Error, string, string, Array<string>>
    let enabled: boolean
    let count: number
    let key: Array<string>

    beforeEach(() => {
      key = queryKey()
      count = 0
      enabled = false

      observer = new QueryObserver(queryClient, {
        queryKey: key,
        staleTime: Infinity,
        enabled: () => enabled,
        queryFn: async () => {
          await sleep(10)
          count++
          return 'data'
        },
      })
    })

    test('should not fetch on mount', () => {
      const unsubscribe = observer.subscribe(vi.fn())

      // Has not fetched and is not fetching since its disabled
      expect(count).toBe(0)
      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'idle',
        data: undefined,
      })

      unsubscribe()
    })

    test('should not be re-fetched when invalidated with refetchType: all', async () => {
      const unsubscribe = observer.subscribe(vi.fn())

      queryClient.invalidateQueries({ queryKey: key, refetchType: 'all' })

      // So we still expect it to not have fetched and not be fetching
      expect(count).toBe(0)
      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'idle',
        data: undefined,
      })
      await waitFor(() => expect(count).toBe(0))

      unsubscribe()
    })

    test('should still trigger a fetch when refetch is called', async () => {
      const unsubscribe = observer.subscribe(vi.fn())

      expect(enabled).toBe(false)

      // Not the same with explicit refetch, this will override enabled and trigger a fetch anyway
      observer.refetch()

      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'fetching',
        data: undefined,
      })

      await waitFor(() => expect(count).toBe(1))
      expect(observer.getCurrentResult()).toMatchObject({
        status: 'success',
        fetchStatus: 'idle',
        data: 'data',
      })

      unsubscribe()
    })

    test('should fetch if unsubscribed, then enabled returns true, and then re-subscribed', async () => {
      let unsubscribe = observer.subscribe(vi.fn())
      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'idle',
        data: undefined,
      })

      unsubscribe()

      enabled = true

      unsubscribe = observer.subscribe(vi.fn())

      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'fetching',
        data: undefined,
      })

      await waitFor(() => expect(count).toBe(1))

      unsubscribe()
    })

    test('should not be re-fetched if not subscribed to after enabled was toggled to true (fetchStatus: "idle")', async () => {
      const unsubscribe = observer.subscribe(vi.fn())

      // Toggle enabled
      enabled = true

      unsubscribe()

      queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })

      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'idle',
        data: undefined,
      })
      expect(count).toBe(0)
    })

    test('should not be re-fetched if not subscribed to after enabled was toggled to true (fetchStatus: "fetching")', async () => {
      const unsubscribe = observer.subscribe(vi.fn())

      // Toggle enabled
      enabled = true

      queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })

      expect(observer.getCurrentResult()).toMatchObject({
        status: 'pending',
        fetchStatus: 'fetching',
        data: undefined,
      })
      await waitFor(() => expect(count).toBe(1))

      unsubscribe()
    })

    test('should handle that the enabled callback updates the return value', async () => {
      const unsubscribe = observer.subscribe(vi.fn())

      // Toggle enabled
      enabled = true

      queryClient.invalidateQueries({ queryKey: key, refetchType: 'inactive' })

      // should not refetch since it was active and we only refetch inactive
      await waitFor(() => expect(count).toBe(0))

      queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })

      // should refetch since it was active and we refetch active
      await waitFor(() => expect(count).toBe(1))

      // Toggle enabled
      enabled = false

      // should not refetch since it is not active and we only refetch active
      queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })

      await waitFor(() => expect(count).toBe(1))

      unsubscribe()
    })
  })

  test('should be able to read latest data when re-subscribing (but not re-fetching)', async () => {
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      staleTime: Infinity,
      queryFn: async () => {
        await sleep(10)
        count++
        return 'data'
      },
    })

    let unsubscribe = observer.subscribe(vi.fn())

    // unsubscribe before data comes in
    unsubscribe()
    expect(count).toBe(0)
    expect(observer.getCurrentResult()).toMatchObject({
      status: 'pending',
      fetchStatus: 'fetching',
      data: undefined,
    })

    await waitFor(() => expect(count).toBe(1))

    // re-subscribe after data comes in
    unsubscribe = observer.subscribe(vi.fn())

    expect(observer.getCurrentResult()).toMatchObject({
      status: 'success',
      data: 'data',
    })

    unsubscribe()
  })

  test('should notify when switching query', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const results: Array<QueryObserverResult> = []
    const observer = new QueryObserver(queryClient, {
      queryKey: key1,
      queryFn: () => 1,
    })
    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })
    await sleep(1)
    observer.setOptions({ queryKey: key2, queryFn: () => 2 })
    await sleep(1)
    unsubscribe()
    expect(results.length).toBe(4)
    expect(results[0]).toMatchObject({ data: undefined, status: 'pending' })
    expect(results[1]).toMatchObject({ data: 1, status: 'success' })
    expect(results[2]).toMatchObject({ data: undefined, status: 'pending' })
    expect(results[3]).toMatchObject({ data: 2, status: 'success' })
  })

  test('should be able to fetch with a selector', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count: 1 }),
      select: (data) => ({ myCount: data.count }),
    })
    let observerResult
    const unsubscribe = observer.subscribe((result) => {
      expectTypeOf(result).toEqualTypeOf<
        QueryObserverResult<{ myCount: number }>
      >()
      observerResult = result
    })
    await sleep(1)
    unsubscribe()
    expect(observerResult).toMatchObject({ data: { myCount: 1 } })
  })

  test('should be able to fetch with a selector using the fetch method', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count: 1 }),
      select: (data) => ({ myCount: data.count }),
    })
    const observerResult = await observer.refetch()
    expectTypeOf(observerResult.data).toEqualTypeOf<
      { myCount: number } | undefined
    >()
    expect(observerResult.data).toMatchObject({ myCount: 1 })
  })

  test('should be able to fetch with a selector and object syntax', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count: 1 }),
      select: (data) => ({ myCount: data.count }),
    })
    let observerResult
    const unsubscribe = observer.subscribe((result) => {
      observerResult = result
    })
    await sleep(1)
    unsubscribe()
    expect(observerResult).toMatchObject({ data: { myCount: 1 } })
  })

  test('should run the selector again if the data changed', async () => {
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count }),
      select: (data) => {
        count++
        return { myCount: data.count }
      },
    })
    const observerResult1 = await observer.refetch()
    const observerResult2 = await observer.refetch()
    expect(count).toBe(2)
    expect(observerResult1.data).toMatchObject({ myCount: 0 })
    expect(observerResult2.data).toMatchObject({ myCount: 1 })
  })

  test('should run the selector again if the selector changed', async () => {
    const key = queryKey()
    let count = 0
    const results: Array<QueryObserverResult> = []
    const queryFn = () => ({ count: 1 })
    const select1 = (data: ReturnType<typeof queryFn>) => {
      count++
      return { myCount: data.count }
    }
    const select2 = (_data: ReturnType<typeof queryFn>) => {
      count++
      return { myCount: 99 }
    }
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      select: select1,
    })
    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })
    await sleep(1)
    observer.setOptions({
      queryKey: key,
      queryFn,
      select: select2,
    })
    await sleep(1)
    await observer.refetch()
    unsubscribe()
    expect(count).toBe(2)
    expect(results.length).toBe(5)
    expect(results[0]).toMatchObject({
      status: 'pending',
      isFetching: true,
      data: undefined,
    })
    expect(results[1]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: { myCount: 1 },
    })
    expect(results[2]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: { myCount: 99 },
    })
    expect(results[3]).toMatchObject({
      status: 'success',
      isFetching: true,
      data: { myCount: 99 },
    })
    expect(results[4]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: { myCount: 99 },
    })
  })

  test('should not run the selector again if the data and selector did not change', async () => {
    const key = queryKey()
    let count = 0
    const results: Array<QueryObserverResult> = []
    const queryFn = () => ({ count: 1 })
    const select = (data: ReturnType<typeof queryFn>) => {
      count++
      return { myCount: data.count }
    }
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      select,
    })
    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })
    await sleep(1)
    observer.setOptions({
      queryKey: key,
      queryFn,
      select,
    })
    await sleep(1)
    await observer.refetch()
    unsubscribe()
    expect(count).toBe(1)
    expect(results.length).toBe(4)
    expect(results[0]).toMatchObject({
      status: 'pending',
      isFetching: true,
      data: undefined,
    })
    expect(results[1]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: { myCount: 1 },
    })
    expect(results[2]).toMatchObject({
      status: 'success',
      isFetching: true,
      data: { myCount: 1 },
    })
    expect(results[3]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: { myCount: 1 },
    })
  })

  test('should not run the selector again if the data did not change', async () => {
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count: 1 }),
      select: (data) => {
        count++
        return { myCount: data.count }
      },
    })
    const observerResult1 = await observer.refetch()
    const observerResult2 = await observer.refetch()
    expect(count).toBe(1)
    expect(observerResult1.data).toMatchObject({ myCount: 1 })
    expect(observerResult2.data).toMatchObject({ myCount: 1 })
  })

  test('should always run the selector again if selector throws an error and selector is not referentially stable', async () => {
    const key = queryKey()
    const results: Array<QueryObserverResult> = []
    const queryFn = async () => {
      await sleep(10)
      return { count: 1 }
    }
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      select: () => {
        throw new Error('selector error')
      },
    })
    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })
    await sleep(50)
    await observer.refetch()
    unsubscribe()
    expect(results[0]).toMatchObject({
      status: 'pending',
      isFetching: true,
      data: undefined,
    })
    expect(results[1]).toMatchObject({
      status: 'error',
      isFetching: false,
      data: undefined,
    })
    expect(results[2]).toMatchObject({
      status: 'error',
      isFetching: true,
      data: undefined,
    })
    expect(results[3]).toMatchObject({
      status: 'error',
      isFetching: false,
      data: undefined,
    })
  })

  test('should return stale data if selector throws an error', async () => {
    const key = queryKey()
    const results: Array<QueryObserverResult> = []
    let shouldError = false
    const error = new Error('select error')
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      retry: 0,
      queryFn: async () => {
        await sleep(10)
        return shouldError ? 2 : 1
      },
      select: (num) => {
        if (shouldError) {
          throw error
        }
        shouldError = true
        return String(num)
      },
    })

    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })
    await sleep(50)
    await observer.refetch()
    unsubscribe()

    expect(results[0]).toMatchObject({
      status: 'pending',
      isFetching: true,
      data: undefined,
      error: null,
    })
    expect(results[1]).toMatchObject({
      status: 'success',
      isFetching: false,
      data: '1',
      error: null,
    })
    expect(results[2]).toMatchObject({
      status: 'success',
      isFetching: true,
      data: '1',
      error: null,
    })
    expect(results[3]).toMatchObject({
      status: 'error',
      isFetching: false,
      data: '1',
      error,
    })
  })

  test('should structurally share the selector', async () => {
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => ({ count: ++count }),
      select: () => ({ myCount: 1 }),
    })
    const observerResult1 = await observer.refetch()
    const observerResult2 = await observer.refetch()
    expect(count).toBe(2)
    expect(observerResult1.data).toBe(observerResult2.data)
  })

  test('should not trigger a fetch when subscribed and disabled', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      enabled: false,
    })
    const unsubscribe = observer.subscribe(() => undefined)
    await sleep(1)
    unsubscribe()
    expect(queryFn).toHaveBeenCalledTimes(0)
  })

  test('should not trigger a fetch when subscribed and disabled by callback', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      enabled: () => false,
    })
    const unsubscribe = observer.subscribe(() => undefined)
    await sleep(1)
    unsubscribe()
    expect(queryFn).toHaveBeenCalledTimes(0)
  })

  test('should not trigger a fetch when not subscribed', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    new QueryObserver(queryClient, { queryKey: key, queryFn })
    await sleep(1)
    expect(queryFn).toHaveBeenCalledTimes(0)
  })

  test('should be able to watch a query without defining a query function', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const callback = vi.fn()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
    })
    const unsubscribe = observer.subscribe(callback)
    await queryClient.fetchQuery({ queryKey: key, queryFn })
    unsubscribe()
    expect(queryFn).toHaveBeenCalledTimes(1)
    expect(callback).toHaveBeenCalledTimes(2)
  })

  test('should accept unresolved query config in update function', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
    })
    const results: Array<QueryObserverResult<unknown>> = []
    const unsubscribe = observer.subscribe((x) => {
      results.push(x)
    })
    observer.setOptions({ queryKey: key, enabled: false, staleTime: 10 })
    await queryClient.fetchQuery({ queryKey: key, queryFn })
    await sleep(20)
    unsubscribe()
    expect(queryFn).toHaveBeenCalledTimes(1)
    expect(results.length).toBe(2)
    expect(results[0]).toMatchObject({ isStale: false, data: undefined })
    expect(results[1]).toMatchObject({ isStale: false, data: 'data' })
  })

  test('should be able to handle multiple subscribers', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')
    const observer = new QueryObserver<string>(queryClient, {
      queryKey: key,
      enabled: false,
    })
    const results1: Array<QueryObserverResult<string>> = []
    const results2: Array<QueryObserverResult<string>> = []
    const unsubscribe1 = observer.subscribe((x) => {
      results1.push(x)
    })
    const unsubscribe2 = observer.subscribe((x) => {
      results2.push(x)
    })
    await queryClient.fetchQuery({ queryKey: key, queryFn })
    await sleep(50)
    unsubscribe1()
    unsubscribe2()
    expect(queryFn).toHaveBeenCalledTimes(1)
    expect(results1.length).toBe(2)
    expect(results2.length).toBe(2)
    expect(results1[0]).toMatchObject({ data: undefined })
    expect(results1[1]).toMatchObject({ data: 'data' })
    expect(results2[0]).toMatchObject({ data: undefined })
    expect(results2[1]).toMatchObject({ data: 'data' })
  })

  test('should stop retry when unsubscribing', async () => {
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => {
        count++
        return Promise.reject<unknown>('reject')
      },
      retry: 10,
      retryDelay: 50,
    })
    const unsubscribe = observer.subscribe(() => undefined)
    await sleep(70)
    unsubscribe()
    await sleep(200)
    expect(count).toBe(2)
  })

  test('should clear interval when unsubscribing to a refetchInterval query', async () => {
    const key = queryKey()
    let count = 0

    const fetchData = () => {
      count++
      return Promise.resolve('data')
    }
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: fetchData,
      gcTime: 0,
      refetchInterval: 10,
    })
    const unsubscribe = observer.subscribe(() => undefined)
    expect(count).toBe(1)
    await sleep(15)
    expect(count).toBe(2)
    unsubscribe()
    await sleep(10)
    expect(queryClient.getQueryCache().find({ queryKey: key })).toBeUndefined()
    expect(count).toBe(2)
  })

  test('uses placeholderData as non-cache data when pending a query with no data', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => 'data',
      placeholderData: 'placeholder',
    })

    expect(observer.getCurrentResult()).toMatchObject({
      status: 'success',
      data: 'placeholder',
    })

    const results: Array<QueryObserverResult<unknown>> = []

    const unsubscribe = observer.subscribe((x) => {
      results.push(x)
    })

    await sleep(10)
    unsubscribe()

    expect(results.length).toBe(2)
    expect(results[0]).toMatchObject({ status: 'success', data: 'placeholder' })
    expect(results[1]).toMatchObject({ status: 'success', data: 'data' })
  })

  test('should structurally share placeholder data', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
      queryFn: () => 'data',
      placeholderData: {},
    })

    const firstData = observer.getCurrentResult().data

    observer.setOptions({ queryKey: key, placeholderData: {} })

    const secondData = observer.getCurrentResult().data

    expect(firstData).toBe(secondData)
  })

  test('should throw an error if enabled option type is not valid', async () => {
    const key = queryKey()

    expect(
      () =>
        new QueryObserver(queryClient, {
          queryKey: key,
          queryFn: () => 'data',
          // @ts-expect-error
          enabled: null,
        }),
    ).toThrowError('Expected enabled to be a boolean')
  })

  test('getCurrentQuery should return the current query', async () => {
    const key = queryKey()

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => 'data',
    })

    expect(observer.getCurrentQuery().queryKey).toEqual(key)
  })

  test('should throw an error if throwOnError option is true', async () => {
    const key = queryKey()

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => Promise.reject<unknown>('error'),
      retry: false,
    })

    let error: string | null = null
    try {
      await observer.refetch({ throwOnError: true })
    } catch (err) {
      error = err as string
    }

    expect(error).toEqual('error')
  })

  test('should not refetch in background if refetchIntervalInBackground is false', async () => {
    const key = queryKey()
    const queryFn = vi
      .fn<(...args: Array<unknown>) => string>()
      .mockReturnValue('data')

    focusManager.setFocused(false)
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn,
      refetchIntervalInBackground: false,
      refetchInterval: 10,
    })

    const unsubscribe = observer.subscribe(() => undefined)
    await sleep(30)

    expect(queryFn).toHaveBeenCalledTimes(1)

    // Clean-up
    unsubscribe()
    focusManager.setFocused(true)
  })

  test('should not use replaceEqualDeep for select value when structuralSharing option is true', async () => {
    const key = queryKey()

    const data = { value: 'data' }
    const selectedData = { value: 'data' }

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => data,
      select: () => data,
    })

    const unsubscribe = observer.subscribe(() => undefined)

    await sleep(10)
    expect(observer.getCurrentResult().data).toBe(data)

    observer.setOptions({
      queryKey: key,
      queryFn: () => data,
      structuralSharing: false,
      select: () => selectedData,
    })

    await observer.refetch()
    expect(observer.getCurrentResult().data).toBe(selectedData)

    unsubscribe()
  })

  test('should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderData is defined', () => {
    const key = queryKey()

    const data = { value: 'data' }
    const selectedData1 = { value: 'data' }
    const selectedData2 = { value: 'data' }
    const placeholderData1 = { value: 'data' }
    const placeholderData2 = { value: 'data' }

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => data,
      select: () => data,
    })

    observer.setOptions({
      queryKey: key,
      queryFn: () => data,
      select: () => {
        return selectedData1
      },
      placeholderData: placeholderData1,
    })

    observer.setOptions({
      queryKey: key,
      queryFn: () => data,
      select: () => {
        return selectedData2
      },
      placeholderData: placeholderData2,
      structuralSharing: false,
    })

    expect(observer.getCurrentResult().data).toBe(selectedData2)
  })

  test('should not use an undefined value returned by select as placeholderData', () => {
    const key = queryKey()

    const data = { value: 'data' }
    const selectedData = { value: 'data' }
    const placeholderData1 = { value: 'data' }
    const placeholderData2 = { value: 'data' }

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => data,
      select: () => data,
    })

    observer.setOptions({
      queryKey: key,
      queryFn: () => data,
      select: () => {
        return selectedData
      },
      placeholderData: placeholderData1,
    })

    expect(observer.getCurrentResult().isPlaceholderData).toBe(true)

    observer.setOptions({
      queryKey: key,
      queryFn: () => data,
      // @ts-expect-error
      select: () => undefined,
      placeholderData: placeholderData2,
    })

    expect(observer.getCurrentResult().isPlaceholderData).toBe(false)
  })

  test('should pass the correct previous queryKey (from prevQuery) to placeholderData function params with select', async () => {
    const results: Array<QueryObserverResult> = []
    const keys: Array<ReadonlyArray<unknown> | null> = []

    const key1 = queryKey()
    const key2 = queryKey()

    const data1 = { value: 'data1' }
    const data2 = { value: 'data2' }

    const observer = new QueryObserver(queryClient, {
      queryKey: key1,
      queryFn: () => data1,
      placeholderData: (prev, prevQuery) => {
        keys.push(prevQuery?.queryKey || null)
        return prev
      },
      select: (data) => data.value,
    })

    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })

    await sleep(1)

    observer.setOptions({
      queryKey: key2,
      queryFn: () => data2,
      placeholderData: (prev, prevQuery) => {
        keys.push(prevQuery?.queryKey || null)
        return prev
      },
      select: (data) => data.value,
    })

    await sleep(1)
    unsubscribe()
    expect(results.length).toBe(4)
    expect(keys.length).toBe(3)
    expect(keys[0]).toBe(null) // First Query - status: 'pending', fetchStatus: 'idle'
    expect(keys[1]).toBe(null) // First Query - status: 'pending', fetchStatus: 'fetching'
    expect(keys[2]).toBe(key1) // Second Query - status: 'pending', fetchStatus: 'fetching'

    expect(results[0]).toMatchObject({
      data: undefined,
      status: 'pending',
      fetchStatus: 'fetching',
    }) // Initial fetch
    expect(results[1]).toMatchObject({
      data: 'data1',
      status: 'success',
      fetchStatus: 'idle',
    }) // Successful fetch
    expect(results[2]).toMatchObject({
      data: 'data1',
      status: 'success',
      fetchStatus: 'fetching',
    }) // Fetch for new key, but using previous data as placeholder
    expect(results[3]).toMatchObject({
      data: 'data2',
      status: 'success',
      fetchStatus: 'idle',
    }) // Successful fetch for new key
  })

  test('should pass the correct previous data to placeholderData function params when select function is used in conjunction', async () => {
    const results: Array<QueryObserverResult> = []

    const key1 = queryKey()
    const key2 = queryKey()

    const data1 = { value: 'data1' }
    const data2 = { value: 'data2' }

    const observer = new QueryObserver(queryClient, {
      queryKey: key1,
      queryFn: () => data1,
      placeholderData: (prev) => prev,
      select: (data) => data.value,
    })

    const unsubscribe = observer.subscribe((result) => {
      results.push(result)
    })

    await sleep(1)

    observer.setOptions({
      queryKey: key2,
      queryFn: () => data2,
      placeholderData: (prev) => prev,
      select: (data) => data.value,
    })

    await sleep(1)
    unsubscribe()

    expect(results.length).toBe(4)
    expect(results[0]).toMatchObject({
      data: undefined,
      status: 'pending',
      fetchStatus: 'fetching',
    }) // Initial fetch
    expect(results[1]).toMatchObject({
      data: 'data1',
      status: 'success',
      fetchStatus: 'idle',
    }) // Successful fetch
    expect(results[2]).toMatchObject({
      data: 'data1',
      status: 'success',
      fetchStatus: 'fetching',
    }) // Fetch for new key, but using previous data as placeholder
    expect(results[3]).toMatchObject({
      data: 'data2',
      status: 'success',
      fetchStatus: 'idle',
    }) // Successful fetch for new key
  })

  test('setOptions should notify cache listeners', async () => {
    const key = queryKey()

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
    })

    const spy = vi.fn()
    const unsubscribe = queryClient.getQueryCache().subscribe(spy)
    observer.setOptions({ queryKey: key, enabled: false, refetchInterval: 10 })

    expect(spy).toHaveBeenCalledTimes(1)
    expect(spy).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'observerOptionsUpdated' }),
    )

    unsubscribe()
  })

  test('disabled observers should not be stale', async () => {
    const key = queryKey()

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
    })

    const result = observer.getCurrentResult()
    expect(result.isStale).toBe(false)
  })

  test('should allow staleTime as a function', async () => {
    const key = queryKey()
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: async () => {
        await sleep(5)
        return {
          data: 'data',
          staleTime: 20,
        }
      },
      staleTime: (query) => query.state.data?.staleTime ?? 0,
    })
    const results: Array<QueryObserverResult<unknown>> = []
    const unsubscribe = observer.subscribe((x) => {
      if (x.data) {
        results.push(x)
      }
    })

    await waitFor(() => expect(results[0]?.isStale).toBe(false))
    await waitFor(() => expect(results[1]?.isStale).toBe(true))

    unsubscribe()
  })

  test('should return a promise that resolves when data is present', async () => {
    const results: Array<QueryObserverResult> = []
    const key = queryKey()
    let count = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => {
        if (++count > 9) {
          return Promise.resolve('data')
        }
        throw new Error('rejected')
      },
      retry: 10,
      retryDelay: 0,
    })
    const unsubscribe = observer.subscribe(() => {
      results.push(observer.getCurrentResult())
    })

    await waitFor(() => {
      expect(results.at(-1)?.data).toBe('data')
    })

    const numberOfUniquePromises = new Set(
      results.map((result) => result.promise),
    ).size
    expect(numberOfUniquePromises).toBe(1)

    unsubscribe()
  })

  test('should return a new promise after recovering from an error', async () => {
    const results: Array<QueryObserverResult> = []
    const key = queryKey()

    let succeeds = false
    let idx = 0
    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      queryFn: () => {
        if (succeeds) {
          return Promise.resolve('data')
        }
        throw new Error(`rejected #${++idx}`)
      },
      retry: 5,
      retryDelay: 0,
    })
    const unsubscribe = observer.subscribe(() => {
      results.push(observer.getCurrentResult())
    })

    await waitFor(() => {
      expect(results.at(-1)?.status).toBe('error')
    })

    expect(
      results.every((result) => result.promise === results[0]!.promise),
    ).toBe(true)

    {
      // fail again
      const lengthBefore = results.length
      observer.refetch()
      await waitFor(() => {
        expect(results.length).toBeGreaterThan(lengthBefore)
        expect(results.at(-1)?.status).toBe('error')
      })

      const numberOfUniquePromises = new Set(
        results.map((result) => result.promise),
      ).size

      expect(numberOfUniquePromises).toBe(2)
    }
    {
      // succeed
      succeeds = true
      observer.refetch()

      await waitFor(() => {
        results.at(-1)?.status === 'success'
      })

      const numberOfUniquePromises = new Set(
        results.map((result) => result.promise),
      ).size

      expect(numberOfUniquePromises).toBe(3)
    }

    unsubscribe()
  })

  test('switching enabled state should reuse the same promise', async () => {
    const key = queryKey()

    const observer = new QueryObserver(queryClient, {
      queryKey: key,
      enabled: false,
      queryFn: () => 'data',
    })
    const results: Array<QueryObserverResult> = []

    const success = pendingThenable<void>()

    const unsubscribe = observer.subscribe((result) => {
      results.push(result)

      if (result.status === 'success') {
        success.resolve()
      }
    })

    observer.setOptions({
      queryKey: key,
      queryFn: () => 'data',
      enabled: true,
    })

    await success

    unsubscribe()

    const promises = new Set(results.map((result) => result.promise))
    expect(promises.size).toBe(1)
  })
})