import { noop } from '@internal/listenerMiddleware/utils'
import { server } from '@internal/query/tests/mocks/server'
import {
  getSerializedHeaders,
  setupApiStore,
} from '@internal/tests/utils/helpers'
import type { SerializedError } from '@reduxjs/toolkit'
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
import type {
  DefinitionsFromApi,
  FetchBaseQueryError,
  FetchBaseQueryMeta,
  OverrideResultType,
  SchemaFailureConverter,
  SchemaType,
  SerializeQueryArgs,
  TagTypesFromApi,
} from '@reduxjs/toolkit/query'
import {
  createApi,
  fetchBaseQuery,
  NamedSchemaError,
} from '@reduxjs/toolkit/query'
import { HttpResponse, delay, http } from 'msw'
import nodeFetch from 'node-fetch'
import * as v from 'valibot'
import type { SchemaFailureHandler } from '../endpointDefinitions'

beforeAll(() => {
  vi.stubEnv('NODE_ENV', 'development')

  return vi.unstubAllEnvs
})

const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)

afterEach(() => {
  vi.clearAllMocks()
  server.resetHandlers()
})

afterAll(() => {
  vi.restoreAllMocks()
})

function paginate<T>(array: T[], page_size: number, page_number: number) {
  // human-readable page numbers usually start with 1, so we reduce 1 in the first argument
  return array.slice((page_number - 1) * page_size, page_number * page_size)
}

test('sensible defaults', () => {
  const api = createApi({
    baseQuery: fetchBaseQuery(),
    endpoints: (build) => ({
      getUser: build.query<unknown, void>({
        query(id) {
          return { url: `user/${id}` }
        },
      }),
      updateUser: build.mutation<unknown, void>({
        query: () => '',
      }),
    }),
  })
  configureStore({
    reducer: {
      [api.reducerPath]: api.reducer,
    },
    middleware: (gDM) => gDM().concat(api.middleware),
  })
  expect(api.reducerPath).toBe('api')

  expect(api.endpoints.getUser.name).toBe('getUser')
  expect(api.endpoints.updateUser.name).toBe('updateUser')
})

describe('wrong tagTypes log errors', () => {
  const baseQuery = vi.fn()
  const api = createApi({
    baseQuery,
    tagTypes: ['User'],
    endpoints: (build) => ({
      provideNothing: build.query<unknown, void>({
        query: () => '',
      }),
      provideTypeString: build.query<unknown, void>({
        query: () => '',
        providesTags: ['User'],
      }),
      provideTypeWithId: build.query<unknown, void>({
        query: () => '',
        providesTags: [{ type: 'User', id: 5 }],
      }),
      provideTypeWithIdAndCallback: build.query<unknown, void>({
        query: () => '',
        providesTags: () => [{ type: 'User', id: 5 }],
      }),
      provideWrongTypeString: build.query<unknown, void>({
        query: () => '',
        // @ts-expect-error
        providesTags: ['Users'],
      }),
      provideWrongTypeWithId: build.query<unknown, void>({
        query: () => '',
        // @ts-expect-error
        providesTags: [{ type: 'Users', id: 5 }],
      }),
      provideWrongTypeWithIdAndCallback: build.query<unknown, void>({
        query: () => '',
        // @ts-expect-error
        providesTags: () => [{ type: 'Users', id: 5 }],
      }),
      invalidateNothing: build.query<unknown, void>({
        query: () => '',
      }),
      invalidateTypeString: build.mutation<unknown, void>({
        query: () => '',
        invalidatesTags: ['User'],
      }),
      invalidateTypeWithId: build.mutation<unknown, void>({
        query: () => '',
        invalidatesTags: [{ type: 'User', id: 5 }],
      }),
      invalidateTypeWithIdAndCallback: build.mutation<unknown, void>({
        query: () => '',
        invalidatesTags: () => [{ type: 'User', id: 5 }],
      }),

      invalidateWrongTypeString: build.mutation<unknown, void>({
        query: () => '',
        // @ts-expect-error
        invalidatesTags: ['Users'],
      }),
      invalidateWrongTypeWithId: build.mutation<unknown, void>({
        query: () => '',
        // @ts-expect-error
        invalidatesTags: [{ type: 'Users', id: 5 }],
      }),
      invalidateWrongTypeWithIdAndCallback: build.mutation<unknown, void>({
        query: () => '',
        // @ts-expect-error
        invalidatesTags: () => [{ type: 'Users', id: 5 }],
      }),
    }),
  })
  const store = configureStore({
    reducer: {
      [api.reducerPath]: api.reducer,
    },
    middleware: (gDM) => gDM().concat(api.middleware),
  })

  beforeEach(() => {
    baseQuery.mockResolvedValue({ data: 'foo' })
  })

  test.each<[keyof typeof api.endpoints, boolean?]>([
    ['provideNothing', false],
    ['provideTypeString', false],
    ['provideTypeWithId', false],
    ['provideTypeWithIdAndCallback', false],
    ['provideWrongTypeString', true],
    ['provideWrongTypeWithId', true],
    ['provideWrongTypeWithIdAndCallback', true],
    ['invalidateNothing', false],
    ['invalidateTypeString', false],
    ['invalidateTypeWithId', false],
    ['invalidateTypeWithIdAndCallback', false],
    ['invalidateWrongTypeString', true],
    ['invalidateWrongTypeWithId', true],
    ['invalidateWrongTypeWithIdAndCallback', true],
  ])(`endpoint %s should log an error? %s`, async (endpoint, shouldError) => {
    vi.stubEnv('NODE_ENV', 'development')

    // @ts-ignore
    store.dispatch(api.endpoints[endpoint].initiate())
    let result: { status: string }
    do {
      await delay(5)
      // @ts-ignore
      result = api.endpoints[endpoint].select()(store.getState())
    } while (result.status === 'pending')

    if (shouldError) {
      expect(consoleErrorSpy).toHaveBeenLastCalledWith(
        "Tag type 'Users' was used, but not specified in `tagTypes`!",
      )
    } else {
      expect(consoleErrorSpy).not.toHaveBeenCalled()
    }
  })
})

describe('endpoint definition typings', () => {
  const api = createApi({
    baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({
      data: 'To',
    }),
    endpoints: () => ({}),
    tagTypes: ['typeA', 'typeB'],
  })
  test('query: query & transformResponse types', () => {
    api.injectEndpoints({
      endpoints: (build) => ({
        query: build.query<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query1: build.query<'RetVal', 'Arg'>({
          // @ts-expect-error
          query: (x: 'Error') => 'From' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query2: build.query<'RetVal', 'Arg'>({
          // @ts-expect-error
          query: (x: 'Arg') => 'Error' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query3: build.query<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          // @ts-expect-error
          transformResponse(r: 'Error') {
            return 'RetVal' as const
          },
        }),
        query4: build.query<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          // @ts-expect-error
          transformResponse(r: 'To') {
            return 'Error' as const
          },
        }),
        queryInference1: build.query<'RetVal', 'Arg'>({
          query: (x) => {
            return 'From'
          },
          transformResponse(r) {
            return 'RetVal'
          },
        }),
        queryInference2: (() => {
          const query = build.query({
            query: (x: 'Arg') => 'From' as const,
            transformResponse(r: 'To') {
              return 'RetVal' as const
            },
          })
          return query
        })(),
      }),
    })
  })
  test('mutation: query & transformResponse types', () => {
    api.injectEndpoints({
      endpoints: (build) => ({
        query: build.mutation<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query1: build.mutation<'RetVal', 'Arg'>({
          // @ts-expect-error
          query: (x: 'Error') => 'From' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query2: build.mutation<'RetVal', 'Arg'>({
          // @ts-expect-error
          query: (x: 'Arg') => 'Error' as const,
          transformResponse(r: 'To') {
            return 'RetVal' as const
          },
        }),
        query3: build.mutation<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          // @ts-expect-error
          transformResponse(r: 'Error') {
            return 'RetVal' as const
          },
        }),
        query4: build.mutation<'RetVal', 'Arg'>({
          query: (x: 'Arg') => 'From' as const,
          // @ts-expect-error
          transformResponse(r: 'To') {
            return 'Error' as const
          },
        }),
        mutationInference1: build.mutation<'RetVal', 'Arg'>({
          query: (x) => {
            return 'From'
          },
          transformResponse(r) {
            return 'RetVal'
          },
        }),
        mutationInference2: (() => {
          const query = build.mutation({
            query: (x: 'Arg') => 'From' as const,
            transformResponse(r: 'To') {
              return 'RetVal' as const
            },
          })
          return query
        })(),
      }),
    })
  })

  describe('enhancing endpoint definitions', () => {
    const baseQuery = vi.fn((x: string) => ({ data: 'success' }))
    const commonBaseQueryApi = {
      dispatch: expect.any(Function),
      endpoint: expect.any(String),
      abort: expect.any(Function),
      extra: undefined,
      forced: expect.any(Boolean),
      getState: expect.any(Function),
      signal: expect.any(Object),
      type: expect.any(String),
      queryCacheKey: expect.any(String),
    }
    beforeEach(() => {
      baseQuery.mockClear()
    })
    function getNewApi() {
      return createApi({
        baseQuery,
        tagTypes: ['old'],
        endpoints: (build) => ({
          query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }),
          query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }),
          mutation1: build.mutation<'out1', 'in1'>({ query: (id) => `${id}` }),
          mutation2: build.mutation<'out2', 'in2'>({ query: (id) => `${id}` }),
        }),
      })
    }
    let api = getNewApi()
    beforeEach(() => {
      api = getNewApi()
    })

    test('pre-modification behavior', async () => {
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
      storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
      storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
      storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))

      expect(baseQuery.mock.calls).toEqual([
        [
          'in1',
          {
            dispatch: expect.any(Function),
            endpoint: expect.any(String),
            getState: expect.any(Function),
            signal: expect.any(Object),
            abort: expect.any(Function),
            forced: expect.any(Boolean),
            type: expect.any(String),
            queryCacheKey: expect.any(String),
          },
          undefined,
        ],
        [
          'in2',
          {
            dispatch: expect.any(Function),
            endpoint: expect.any(String),
            getState: expect.any(Function),
            signal: expect.any(Object),
            abort: expect.any(Function),
            forced: expect.any(Boolean),
            type: expect.any(String),
            queryCacheKey: expect.any(String),
          },
          undefined,
        ],
        [
          'in1',
          {
            dispatch: expect.any(Function),
            endpoint: expect.any(String),
            getState: expect.any(Function),
            signal: expect.any(Object),
            abort: expect.any(Function),
            // forced: undefined,
            type: expect.any(String),
          },
          undefined,
        ],
        [
          'in2',
          {
            dispatch: expect.any(Function),
            endpoint: expect.any(String),
            getState: expect.any(Function),
            signal: expect.any(Object),
            abort: expect.any(Function),
            // forced: undefined,
            type: expect.any(String),
          },
          undefined,
        ],
      ])
    })

    test('warn on wrong tagType', async () => {
      vi.stubEnv('NODE_ENV', 'development')

      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      // only type-test this part
      if (2 > 1) {
        api.enhanceEndpoints({
          endpoints: {
            query1: {
              // @ts-expect-error
              providesTags: ['new'],
            },
            query2: {
              // @ts-expect-error
              providesTags: ['missing'],
            },
          },
        })
      }

      const enhanced = api.enhanceEndpoints({
        addTagTypes: ['new'],
        endpoints: {
          query1: {
            providesTags: ['new'],
          },
          query2: {
            // @ts-expect-error
            providesTags: ['missing'],
          },
        },
      })

      storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
      await delay(1)
      expect(consoleErrorSpy).not.toHaveBeenCalled()

      storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
      await delay(1)

      expect(consoleErrorSpy).toHaveBeenCalledOnce()

      expect(consoleErrorSpy).toHaveBeenLastCalledWith(
        "Tag type 'missing' was used, but not specified in `tagTypes`!",
      )

      // only type-test this part
      if (2 > 1) {
        enhanced.enhanceEndpoints({
          endpoints: {
            query1: {
              // returned `enhanced` api contains "new" enitityType
              providesTags: ['new'],
            },
            query2: {
              // @ts-expect-error
              providesTags: ['missing'],
            },
          },
        })
      }
    })

    test('modify', () => {
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      api.enhanceEndpoints({
        endpoints: {
          query1: {
            query: (x) => {
              return 'modified1'
            },
          },
          query2(definition) {
            definition.query = (x) => {
              return 'modified2'
            }
          },
          mutation1: {
            query: (x) => {
              return 'modified1'
            },
          },
          mutation2(definition) {
            definition.query = (x) => {
              return 'modified2'
            }
          },
          // @ts-expect-error
          nonExisting: {},
        },
      })

      storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
      storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
      storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
      storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))

      expect(baseQuery.mock.calls).toEqual([
        ['modified1', commonBaseQueryApi, undefined],
        ['modified2', commonBaseQueryApi, undefined],
        [
          'modified1',
          {
            ...commonBaseQueryApi,
            forced: undefined,
            queryCacheKey: undefined,
          },
          undefined,
        ],
        [
          'modified2',
          {
            ...commonBaseQueryApi,
            forced: undefined,
            queryCacheKey: undefined,
          },
          undefined,
        ],
      ])
    })

    test('updated transform response types', async () => {
      const baseApi = createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        tagTypes: ['old'],
        endpoints: (build) => ({
          query1: build.query<'out1', void>({ query: () => 'success' }),
          mutation1: build.mutation<'out1', void>({ query: () => 'success' }),
        }),
      })

      type Transformed = { value: string }

      type Definitions = DefinitionsFromApi<typeof api>
      type TagTypes = TagTypesFromApi<typeof api>

      type Q1Definition = OverrideResultType<Definitions['query1'], Transformed>
      type M1Definition = OverrideResultType<
        Definitions['mutation1'],
        Transformed
      >

      type UpdatedDefitions = Omit<Definitions, 'query1' | 'mutation1'> & {
        query1: Q1Definition
        mutation1: M1Definition
      }

      const enhancedApi = baseApi.enhanceEndpoints<TagTypes, UpdatedDefitions>({
        endpoints: {
          query1: {
            transformResponse: (a, b, c) => ({
              value: 'transformed',
            }),
          },
          mutation1: {
            transformResponse: (a, b, c) => ({
              value: 'transformed',
            }),
          },
        },
      })

      const storeRef = setupApiStore(enhancedApi, undefined, {
        withoutTestLifecycles: true,
      })

      const queryResponse = await storeRef.store.dispatch(
        enhancedApi.endpoints.query1.initiate(),
      )
      expect(queryResponse.data).toEqual({ value: 'transformed' })

      const mutationResponse = await storeRef.store.dispatch(
        enhancedApi.endpoints.mutation1.initiate(),
      )
      expect('data' in mutationResponse && mutationResponse.data).toEqual({
        value: 'transformed',
      })
    })
  })
})

describe('additional transformResponse behaviors', () => {
  type SuccessResponse = { value: 'success' }
  type EchoResponseData = { banana: 'bread' }
  type ErrorResponse = { value: 'error' }
  const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
    endpoints: (build) => ({
      echo: build.mutation({
        query: () => ({ method: 'PUT', url: '/echo' }),
      }),
      mutation: build.mutation({
        query: () => ({
          url: '/echo',
          method: 'POST',
          body: { nested: { banana: 'bread' } },
        }),
        transformResponse: (response: { body: { nested: EchoResponseData } }) =>
          response.body.nested,
      }),
      mutationWithError: build.mutation({
        query: () => ({
          url: '/error',
          method: 'POST',
        }),
        transformErrorResponse: (response) => {
          const data = response.data as ErrorResponse
          return data.value
        },
      }),
      mutationWithMeta: build.mutation({
        query: () => ({
          url: '/echo',
          method: 'POST',
          body: { nested: { banana: 'bread' } },
        }),
        transformResponse: (
          response: { body: { nested: EchoResponseData } },
          meta,
        ) => {
          return {
            ...response.body.nested,
            meta: {
              request: { headers: getSerializedHeaders(meta?.request.headers) },
              response: {
                headers: getSerializedHeaders(meta?.response?.headers),
              },
            },
          }
        },
      }),
      query: build.query<SuccessResponse & EchoResponseData, void>({
        query: () => '/success',
        transformResponse: async (response: SuccessResponse) => {
          const res: any = await nodeFetch('https://example.com/echo', {
            method: 'POST',
            body: JSON.stringify({ banana: 'bread' }),
          }).then((res) => res.json())

          const additionalData = res.body as EchoResponseData
          return { ...response, ...additionalData }
        },
      }),
      queryWithMeta: build.query<SuccessResponse, void>({
        query: () => '/success',
        transformResponse: async (response: SuccessResponse, meta) => {
          return {
            ...response,
            meta: {
              request: { headers: getSerializedHeaders(meta?.request.headers) },
              response: {
                headers: getSerializedHeaders(meta?.response?.headers),
              },
            },
          }
        },
      }),
    }),
  })

  const storeRef = setupApiStore(api)

  test('transformResponse handles an async transformation and returns the merged data (query)', async () => {
    const result = await storeRef.store.dispatch(api.endpoints.query.initiate())

    expect(result.data).toEqual({ value: 'success', banana: 'bread' })
  })

  test('transformResponse transforms a response from a mutation', async () => {
    const result = await storeRef.store.dispatch(
      api.endpoints.mutation.initiate({}),
    )

    expect('data' in result && result.data).toEqual({ banana: 'bread' })
  })

  test('transformResponse transforms a response from a mutation with an error', async () => {
    const result = await storeRef.store.dispatch(
      api.endpoints.mutationWithError.initiate({}),
    )

    expect('error' in result && result.error).toEqual('error')
  })

  test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => {
    const result = await storeRef.store.dispatch(
      api.endpoints.mutationWithMeta.initiate({}),
    )

    expect('data' in result && result.data).toEqual({
      banana: 'bread',
      meta: {
        request: {
          headers: {
            'content-type': 'application/json',
          },
        },
        response: {
          headers: {
            'content-type': 'application/json',
          },
        },
      },
    })
  })

  test('transformResponse can inject baseQuery meta into the end result from a query', async () => {
    const result = await storeRef.store.dispatch(
      api.endpoints.queryWithMeta.initiate(),
    )

    expect(result.data).toEqual({
      value: 'success',
      meta: {
        request: {
          headers: {},
        },
        response: {
          headers: {
            'content-type': 'application/json',
          },
        },
      },
    })
  })
})

describe('query endpoint lifecycles - onStart, onSuccess, onError', () => {
  const initialState = {
    count: null as null | number,
  }
  const setCount = createAction<number>('setCount')
  const testReducer = createReducer(initialState, (builder) => {
    builder.addCase(setCount, (state, action) => {
      state.count = action.payload
    })
  })

  type SuccessResponse = { value: 'success' }
  const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
    endpoints: (build) => ({
      echo: build.mutation({
        query: () => ({ method: 'PUT', url: '/echo' }),
      }),
      query: build.query<SuccessResponse, void>({
        query: () => '/success',
        async onQueryStarted(_, api) {
          api.dispatch(setCount(0))
          try {
            await api.queryFulfilled
            api.dispatch(setCount(1))
          } catch {
            api.dispatch(setCount(-1))
          }
        },
      }),
      mutation: build.mutation<SuccessResponse, void>({
        query: () => ({ url: '/success', method: 'POST' }),
        async onQueryStarted(_, api) {
          api.dispatch(setCount(0))
          try {
            await api.queryFulfilled
            api.dispatch(setCount(1))
          } catch {
            api.dispatch(setCount(-1))
          }
        },
      }),
    }),
  })

  const storeRef = setupApiStore(api, { testReducer })

  test('query lifecycle events fire properly', async () => {
    // We intentionally fail the first request so we can test all lifecycles
    server.use(
      http.get(
        'https://example.com/success',
        () => HttpResponse.json({ value: 'failed' }, { status: 500 }),
        { once: true },
      ),
    )

    expect(storeRef.store.getState().testReducer.count).toBe(null)
    const failAttempt = storeRef.store.dispatch(api.endpoints.query.initiate())
    expect(storeRef.store.getState().testReducer.count).toBe(0)
    await failAttempt
    await delay(10)
    expect(storeRef.store.getState().testReducer.count).toBe(-1)

    const successAttempt = storeRef.store.dispatch(
      api.endpoints.query.initiate(),
    )
    expect(storeRef.store.getState().testReducer.count).toBe(0)
    await successAttempt
    await delay(10)
    expect(storeRef.store.getState().testReducer.count).toBe(1)
  })

  test('mutation lifecycle events fire properly', async () => {
    // We intentionally fail the first request so we can test all lifecycles
    server.use(
      http.post(
        'https://example.com/success',
        () => HttpResponse.json({ value: 'failed' }, { status: 500 }),
        { once: true },
      ),
    )

    expect(storeRef.store.getState().testReducer.count).toBe(null)
    const failAttempt = storeRef.store.dispatch(
      api.endpoints.mutation.initiate(),
    )
    expect(storeRef.store.getState().testReducer.count).toBe(0)
    await failAttempt
    expect(storeRef.store.getState().testReducer.count).toBe(-1)

    const successAttempt = storeRef.store.dispatch(
      api.endpoints.mutation.initiate(),
    )
    expect(storeRef.store.getState().testReducer.count).toBe(0)
    await successAttempt
    expect(storeRef.store.getState().testReducer.count).toBe(1)
  })
})

test('providesTags and invalidatesTags can use baseQueryMeta', async () => {
  let _meta: FetchBaseQueryMeta | undefined

  type SuccessResponse = { value: 'success' }

  const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
    tagTypes: ['success'],
    endpoints: (build) => ({
      query: build.query<SuccessResponse, void>({
        query: () => '/success',
        providesTags: (_result, _error, _arg, meta) => {
          _meta = meta
          return ['success']
        },
      }),
      mutation: build.mutation<SuccessResponse, void>({
        query: () => ({ url: '/success', method: 'POST' }),
        invalidatesTags: (_result, _error, _arg, meta) => {
          _meta = meta
          return ['success']
        },
      }),
    }),
  })

  const storeRef = setupApiStore(api, undefined, {
    withoutTestLifecycles: true,
  })

  await storeRef.store.dispatch(api.endpoints.query.initiate())
  expect('request' in _meta! && 'response' in _meta!).toBe(true)

  _meta = undefined

  await storeRef.store.dispatch(api.endpoints.mutation.initiate())

  expect('request' in _meta! && 'response' in _meta!).toBe(true)
})

describe('structuralSharing flag behaviors', () => {
  type SuccessResponse = { value: 'success' }

  const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
    tagTypes: ['success'],
    endpoints: (build) => ({
      enabled: build.query<SuccessResponse, void>({
        query: () => '/success',
      }),
      disabled: build.query<SuccessResponse, void>({
        query: () => ({ url: '/success' }),
        structuralSharing: false,
      }),
    }),
  })

  const storeRef = setupApiStore(api)

  it('enables structural sharing for query endpoints by default', async () => {
    await storeRef.store.dispatch(api.endpoints.enabled.initiate())
    const firstRef = api.endpoints.enabled.select()(storeRef.store.getState())

    await storeRef.store.dispatch(
      api.endpoints.enabled.initiate(undefined, { forceRefetch: true }),
    )

    const secondRef = api.endpoints.enabled.select()(storeRef.store.getState())

    expect(firstRef.requestId).not.toEqual(secondRef.requestId)
    expect(firstRef.data === secondRef.data).toBeTruthy()
  })

  it('allows a query endpoint to opt-out of structural sharing', async () => {
    await storeRef.store.dispatch(api.endpoints.disabled.initiate())
    const firstRef = api.endpoints.disabled.select()(storeRef.store.getState())

    await storeRef.store.dispatch(
      api.endpoints.disabled.initiate(undefined, { forceRefetch: true }),
    )

    const secondRef = api.endpoints.disabled.select()(storeRef.store.getState())

    expect(firstRef.requestId).not.toEqual(secondRef.requestId)
    expect(firstRef.data === secondRef.data).toBeFalsy()
  })
})

describe('custom serializeQueryArgs per endpoint', () => {
  const customArgsSerializer: SerializeQueryArgs<number> = ({
    endpointName,
    queryArgs,
  }) => `${endpointName}-${queryArgs}`

  type SuccessResponse = { value: 'success' }

  const serializer1 = vi.fn(customArgsSerializer)

  interface MyApiClient {
    fetchPost: (id: string) => Promise<SuccessResponse>
  }

  const dummyClient: MyApiClient = {
    async fetchPost() {
      return { value: 'success' }
    },
  }

  const api = createApi({
    baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
    serializeQueryArgs: ({ endpointName, queryArgs }) =>
      `base-${endpointName}-${queryArgs}`,
    endpoints: (build) => ({
      queryWithNoSerializer: build.query<SuccessResponse, number>({
        query: (arg) => `${arg}`,
      }),
      queryWithCustomSerializer: build.query<SuccessResponse, number>({
        query: (arg) => `${arg}`,
        serializeQueryArgs: serializer1,
      }),
      queryWithCustomObjectSerializer: build.query<
        SuccessResponse,
        { id: number; client: MyApiClient }
      >({
        query: (arg) => `${arg.id}`,
        serializeQueryArgs: ({
          endpointDefinition,
          endpointName,
          queryArgs,
        }) => {
          const { id } = queryArgs
          return { id }
        },
      }),
      queryWithCustomNumberSerializer: build.query<
        SuccessResponse,
        { id: number; client: MyApiClient }
      >({
        query: (arg) => `${arg.id}`,
        serializeQueryArgs: ({
          endpointDefinition,
          endpointName,
          queryArgs,
        }) => {
          const { id } = queryArgs
          return id
        },
      }),
      listItems: build.query<string[], number>({
        query: (pageNumber) => `/listItems?page=${pageNumber}`,
        serializeQueryArgs: ({ endpointName }) => {
          return endpointName
        },
        merge: (currentCache, newItems) => {
          currentCache.push(...newItems)
        },
        forceRefetch({ currentArg, previousArg }) {
          return currentArg !== previousArg
        },
      }),
      listItems2: build.query<{ items: string[]; meta?: any }, number>({
        query: (pageNumber) => `/listItems2?page=${pageNumber}`,
        serializeQueryArgs: ({ endpointName }) => {
          return endpointName
        },
        transformResponse(items: string[]) {
          return { items }
        },
        merge: (currentCache, newData, meta) => {
          currentCache.items.push(...newData.items)
          currentCache.meta = meta
        },
        forceRefetch({ currentArg, previousArg }) {
          return currentArg !== previousArg
        },
      }),
    }),
  })

  const storeRef = setupApiStore(api)

  it('Works via createApi', async () => {
    await storeRef.store.dispatch(
      api.endpoints.queryWithNoSerializer.initiate(99),
    )

    expect(serializer1).not.toHaveBeenCalled()

    await storeRef.store.dispatch(
      api.endpoints.queryWithCustomSerializer.initiate(42),
    )

    expect(serializer1).toHaveBeenCalled()

    expect(
      storeRef.store.getState().api.queries['base-queryWithNoSerializer-99'],
    ).toBeTruthy()

    expect(
      storeRef.store.getState().api.queries['queryWithCustomSerializer-42'],
    ).toBeTruthy()
  })

  const serializer2 = vi.fn(customArgsSerializer)

  const injectedApi = api.injectEndpoints({
    endpoints: (build) => ({
      injectedQueryWithCustomSerializer: build.query<SuccessResponse, number>({
        query: (arg) => `${arg}`,
        serializeQueryArgs: serializer2,
      }),
    }),
  })

  it('Works via injectEndpoints', async () => {
    expect(serializer2).not.toHaveBeenCalled()

    await storeRef.store.dispatch(
      injectedApi.endpoints.injectedQueryWithCustomSerializer.initiate(5),
    )

    expect(serializer2).toHaveBeenCalled()
    expect(
      storeRef.store.getState().api.queries[
        'injectedQueryWithCustomSerializer-5'
      ],
    ).toBeTruthy()
  })

  test('Serializes a returned object for query args', async () => {
    await storeRef.store.dispatch(
      api.endpoints.queryWithCustomObjectSerializer.initiate({
        id: 42,
        client: dummyClient,
      }),
    )

    expect(
      storeRef.store.getState().api.queries[
        'queryWithCustomObjectSerializer({"id":42})'
      ],
    ).toBeTruthy()
  })

  test('Serializes a returned primitive for query args', async () => {
    await storeRef.store.dispatch(
      api.endpoints.queryWithCustomNumberSerializer.initiate({
        id: 42,
        client: dummyClient,
      }),
    )

    expect(
      storeRef.store.getState().api.queries[
        'queryWithCustomNumberSerializer(42)'
      ],
    ).toBeTruthy()
  })

  test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => {
    const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
    const PAGE_SIZE = 3

    server.use(
      http.get('https://example.com/listItems', ({ request }) => {
        const url = new URL(request.url)
        const pageString = url.searchParams.get('page')
        const pageNum = parseInt(pageString || '0')

        const results = paginate(allItems, PAGE_SIZE, pageNum)
        return HttpResponse.json(results)
      }),
    )

    // Page number shouldn't matter here, because the cache key ignores that.
    // We just need to select the only cache entry.
    const selectListItems = api.endpoints.listItems.select(0)

    await storeRef.store.dispatch(api.endpoints.listItems.initiate(1))

    const initialEntry = selectListItems(storeRef.store.getState())
    expect(initialEntry.data).toEqual(['a', 'b', 'c'])

    await storeRef.store.dispatch(api.endpoints.listItems.initiate(2))
    const updatedEntry = selectListItems(storeRef.store.getState())
    expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
  })

  test('merge receives a meta object as an argument', async () => {
    const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
    const PAGE_SIZE = 3

    server.use(
      http.get('https://example.com/listItems2', ({ request }) => {
        const url = new URL(request.url)
        const pageString = url.searchParams.get('page')
        const pageNum = parseInt(pageString || '0')

        const results = paginate(allItems, PAGE_SIZE, pageNum)
        return HttpResponse.json(results)
      }),
    )

    const selectListItems = api.endpoints.listItems2.select(0)

    await storeRef.store.dispatch(api.endpoints.listItems2.initiate(1))
    await storeRef.store.dispatch(api.endpoints.listItems2.initiate(2))
    const cacheEntry = selectListItems(storeRef.store.getState())

    // Should have passed along the third arg from `merge` containing these fields
    expect(cacheEntry.data?.meta).toEqual({
      requestId: expect.any(String),
      fulfilledTimeStamp: expect.any(Number),
      arg: 2,
      baseQueryMeta: expect.any(Object),
    })
  })
})

describe('timeout behavior', () => {
  test('triggers TIMEOUT_ERROR', async () => {
    const api = createApi({
      baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com', timeout: 5 }),
      endpoints: (build) => ({
        query: build.query<unknown, void>({
          query: () => '/success',
        }),
      }),
    })

    const storeRef = setupApiStore(api, undefined, {
      withoutTestLifecycles: true,
    })

    server.use(
      http.get(
        'https://example.com/success',
        async () => {
          await delay(50)
          return HttpResponse.json({ value: 'failed' }, { status: 500 })
        },
        { once: true },
      ),
    )

    const result = await storeRef.store.dispatch(api.endpoints.query.initiate())

    expect(result?.error).toEqual({
      status: 'TIMEOUT_ERROR',
      error: expect.stringMatching(/^AbortError:/),
    })
  })
})

describe('endpoint schemas', () => {
  const schemaConverter: SchemaFailureConverter<
    ReturnType<typeof fetchBaseQuery>
  > = (error) => {
    return {
      status: 'CUSTOM_ERROR',
      error: error.schemaName + ' failed validation',
      data: error.issues,
    }
  }

  const serializedSchemaError = {
    name: 'SchemaError',
    message: expect.any(String),
    stack: expect.any(String),
  } satisfies SerializedError

  const onSchemaFailureGlobal = vi.fn<Parameters<SchemaFailureHandler>>()
  const onSchemaFailureEndpoint = vi.fn<Parameters<SchemaFailureHandler>>()
  afterEach(() => {
    onSchemaFailureGlobal.mockClear()
    onSchemaFailureEndpoint.mockClear()
  })

  function expectFailureHandlersToHaveBeenCalled({
    schemaName,
    value,
    arg,
  }: {
    schemaName: `${SchemaType}Schema`
    value: unknown
    arg: unknown
  }) {
    for (const handler of [onSchemaFailureGlobal, onSchemaFailureEndpoint]) {
      expect(handler).toHaveBeenCalledOnce()
      const [namedError, info] = handler.mock.calls[0]
      expect(namedError).toBeInstanceOf(NamedSchemaError)
      expect(namedError.issues.length).toBeGreaterThan(0)
      expect(namedError.value).toEqual(value)
      expect(namedError.schemaName).toBe(schemaName)
      expect(info.endpoint).toBe('query')
      expect(info.type).toBe('query')
      expect(info.arg).toEqual(arg)
    }
  }

  interface SkipApiOptions {
    globalSkip?: boolean
    endpointSkip?: boolean
    useArray?: boolean
    globalCatch?: boolean
    endpointCatch?: boolean
  }

  const apiOptions = (
    type: SchemaType,
    { useArray, globalSkip, globalCatch }: SkipApiOptions = {},
  ) => ({
    onSchemaFailure: onSchemaFailureGlobal,
    skipSchemaValidation: useArray ? globalSkip && [type] : globalSkip,
    catchSchemaFailure: globalCatch ? schemaConverter : undefined,
  })

  const endpointOptions = (
    type: SchemaType,
    { useArray, endpointSkip, endpointCatch }: SkipApiOptions = {},
  ) => ({
    onSchemaFailure: onSchemaFailureEndpoint,
    skipSchemaValidation: useArray ? endpointSkip && [type] : endpointSkip,
    catchSchemaFailure: endpointCatch ? schemaConverter : undefined,
  })

  const skipCases: [string, SkipApiOptions][] = [
    ['globally', { globalSkip: true }],
    ['on the endpoint', { endpointSkip: true }],
    ['globally (array)', { globalSkip: true, useArray: true }],
    ['on the endpoint (array)', { endpointSkip: true, useArray: true }],
  ]

  describe('argSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('arg', opts),
        endpoints: (build) => ({
          query: build.query<unknown, { id: number }>({
            query: ({ id }) => `/post/${id}`,
            argSchema: v.object({ id: v.number() }),
            ...endpointOptions('arg', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's arguments", async () => {
      const api = makeApi()

      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })

      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate({ id: 1 }),
      )

      expect(result?.error).toBeUndefined()

      const invalidResult = await storeRef.store.dispatch(
        // @ts-expect-error
        api.endpoints.query.initiate({ id: '1' }),
      )

      expect(invalidResult?.error).toEqual(serializedSchemaError)
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'argSchema',
        value: { id: '1' },
        arg: { id: '1' },
      })
    })

    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)

      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })

      const result = await storeRef.store.dispatch(
        // @ts-expect-error
        api.endpoints.query.initiate({ id: '1' }),
      )

      expect(result?.error).toBeUndefined()
    })
    // we only need to test this once
    test('endpoint overrides global skip', async () => {
      const api = makeApi({ globalSkip: true, endpointSkip: false })

      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })

      const result = await storeRef.store.dispatch(
        // @ts-expect-error
        api.endpoints.query.initiate({ id: '1' }),
      )

      expect(result?.error).toEqual(serializedSchemaError)
    })

    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })

      const result = await storeRef.store.dispatch(
        // @ts-expect-error
        api.endpoints.query.initiate({ id: '1' }),
      )

      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'argSchema failed validation',
        data: expect.any(Array),
      })

      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'argSchema',
        value: { id: '1' },
        arg: { id: '1' },
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })

      const result = await storeRef.store.dispatch(
        // @ts-expect-error
        api.endpoints.query.initiate({ id: '1' }),
      )

      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'argSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'argSchema',
        value: { id: '1' },
        arg: { id: '1' },
      })
    })
  })
  describe('rawResponseSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('rawResponse', opts),
        endpoints: (build) => ({
          query: build.query<{ success: boolean }, void>({
            query: () => '/success',
            rawResponseSchema: v.object({ value: v.literal('success!') }),
            ...endpointOptions('rawResponse', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's raw result", async () => {
      const api = makeApi()
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual(serializedSchemaError)
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawResponseSchema',
        value: { value: 'success' },
        arg: undefined,
      })
    })
    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toBeUndefined()
    })
    test('can be skipped on the endpoint', async () => {
      const api = makeApi({ endpointSkip: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toBeUndefined()
    })
    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'rawResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawResponseSchema',
        value: { value: 'success' },
        arg: undefined,
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'rawResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawResponseSchema',
        value: { value: 'success' },
        arg: undefined,
      })
    })
  })
  describe('responseSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('response', opts),
        endpoints: (build) => ({
          query: build.query<{ success: boolean }, void>({
            query: () => '/success',
            transformResponse: () => ({ success: false }),
            responseSchema: v.object({ success: v.literal(true) }),
            ...endpointOptions('response', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's final result", async () => {
      const api = makeApi()
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual(serializedSchemaError)

      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'responseSchema',
        value: { success: false },
        arg: undefined,
      })
    })
    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toBeUndefined()
    })
    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'responseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'responseSchema',
        value: { success: false },
        arg: undefined,
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'responseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'responseSchema',
        value: { success: false },
        arg: undefined,
      })
    })
  })
  describe('rawErrorResponseSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('rawErrorResponse', opts),
        endpoints: (build) => ({
          query: build.query<{ success: boolean }, void>({
            query: () => '/error',
            rawErrorResponseSchema: v.object({
              status: v.pipe(v.number(), v.minValue(400), v.maxValue(499)),
              data: v.unknown(),
            }),
            ...endpointOptions('rawErrorResponse', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's raw error result", async () => {
      const api = makeApi()
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual(serializedSchemaError)
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawErrorResponseSchema',
        value: { status: 500, data: { value: 'error' } },
        arg: undefined,
      })
    })
    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).not.toEqual(serializedSchemaError)
    })
    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'rawErrorResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawErrorResponseSchema',
        value: { status: 500, data: { value: 'error' } },
        arg: undefined,
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'rawErrorResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'rawErrorResponseSchema',
        value: { status: 500, data: { value: 'error' } },
        arg: undefined,
      })
    })
  })
  describe('errorResponseSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('errorResponse', opts),
        endpoints: (build) => ({
          query: build.query<{ success: boolean }, void>({
            query: () => '/error',
            transformErrorResponse: (error): FetchBaseQueryError => ({
              status: 'CUSTOM_ERROR',
              data: error,
              error: 'whoops',
            }),
            errorResponseSchema: v.object({
              status: v.literal('CUSTOM_ERROR'),
              error: v.literal('oh no'),
              data: v.unknown(),
            }),
            ...endpointOptions('errorResponse', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's final error result", async () => {
      const api = makeApi()
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual(serializedSchemaError)
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'errorResponseSchema',
        value: {
          status: 'CUSTOM_ERROR',
          error: 'whoops',
          data: { status: 500, data: { value: 'error' } },
        },
        arg: undefined,
      })
    })
    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).not.toEqual(serializedSchemaError)
    })
    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'errorResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'errorResponseSchema',
        value: {
          status: 'CUSTOM_ERROR',
          error: 'whoops',
          data: { status: 500, data: { value: 'error' } },
        },
        arg: undefined,
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'errorResponseSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'errorResponseSchema',
        value: {
          status: 'CUSTOM_ERROR',
          error: 'whoops',
          data: { status: 500, data: { value: 'error' } },
        },
        arg: undefined,
      })
    })
  })
  describe('metaSchema', () => {
    const makeApi = (opts?: SkipApiOptions) =>
      createApi({
        baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
        ...apiOptions('meta', opts),
        endpoints: (build) => ({
          query: build.query<{ success: boolean }, void>({
            query: () => '/success',
            metaSchema: v.object({
              request: v.instance(Request),
              response: v.instance(Response),
              timestamp: v.number(),
            }),
            ...endpointOptions('meta', opts),
          }),
        }),
      })
    test("can be used to validate the endpoint's meta result", async () => {
      const api = makeApi()
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual(serializedSchemaError)
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'metaSchema',
        value: {
          request: expect.any(Request),
          response: expect.any(Response),
        },
        arg: undefined,
      })
    })
    test.each(skipCases)('can be skipped %s', async (_, arg) => {
      const api = makeApi(arg)
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toBeUndefined()
    })
    test('can be converted to a standard error object at global level', async () => {
      const api = makeApi({ globalCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'metaSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'metaSchema',
        value: {
          request: expect.any(Request),
          response: expect.any(Response),
        },
        arg: undefined,
      })
    })
    test('can be converted to a standard error object at endpoint level', async () => {
      const api = makeApi({ endpointCatch: true })
      const storeRef = setupApiStore(api, undefined, {
        withoutTestLifecycles: true,
      })
      const result = await storeRef.store.dispatch(
        api.endpoints.query.initiate(),
      )
      expect(result?.error).toEqual({
        status: 'CUSTOM_ERROR',
        error: 'metaSchema failed validation',
        data: expect.any(Array),
      })
      expectFailureHandlersToHaveBeenCalled({
        schemaName: 'metaSchema',
        value: {
          request: expect.any(Request),
          response: expect.any(Response),
        },
        arg: undefined,
      })
    })
  })
})
