import { setupApiStore } from '@internal/tests/utils/helpers'
import type { EntityState, SerializedError } from '@reduxjs/toolkit'
import { configureStore, createEntityAdapter } from '@reduxjs/toolkit'
import type {
  DefinitionsFromApi,
  FetchBaseQueryError,
  FetchBaseQueryMeta,
  MutationDefinition,
  OverrideResultType,
  QueryDefinition,
  TagDescription,
  TagTypesFromApi,
} from '@reduxjs/toolkit/query'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import * as v from 'valibot'
import type { Post } from './mocks/handlers'

describe('type tests', () => {
  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),
    })

    expectTypeOf(api.reducerPath).toEqualTypeOf<'api'>()

    expectTypeOf(api.util.invalidateTags)
      .parameter(0)
      .toEqualTypeOf<(null | undefined | TagDescription<never>)[]>()
  })

  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) => {
              expectTypeOf(x).toEqualTypeOf<'Arg'>()

              return 'From'
            },
            transformResponse(r) {
              expectTypeOf(r).toEqualTypeOf<'To'>()

              return 'RetVal'
            },
          }),
          queryInference2: (() => {
            const query = build.query({
              query: (x: 'Arg') => 'From' as const,
              transformResponse(r: 'To') {
                return 'RetVal' as const
              },
            })

            expectTypeOf(query).toMatchTypeOf<
              QueryDefinition<'Arg', any, any, 'RetVal'>
            >()

            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) => {
              expectTypeOf(x).toEqualTypeOf<'Arg'>()

              return 'From'
            },
            transformResponse(r) {
              expectTypeOf(r).toEqualTypeOf<'To'>()

              return 'RetVal'
            },
          }),
          mutationInference2: (() => {
            const query = build.mutation({
              query: (x: 'Arg') => 'From' as const,
              transformResponse(r: 'To') {
                return 'RetVal' as const
              },
            })

            expectTypeOf(query).toMatchTypeOf<
              MutationDefinition<'Arg', any, any, 'RetVal'>
            >()

            return query
          })(),
        }),
      })
    })

    describe('enhancing endpoint definitions', () => {
      const baseQuery = (x: string) => ({ data: 'success' })

      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}`,
            }),
          }),
        })
      }

      const api1 = getNewApi()

      test('warn on wrong tagType', () => {
        const storeRef = setupApiStore(api1, undefined, {
          withoutTestLifecycles: true,
        })

        api1.enhanceEndpoints({
          endpoints: {
            query1: {
              // @ts-expect-error
              providesTags: ['new'],
            },
            query2: {
              // @ts-expect-error
              providesTags: ['missing'],
            },
          },
        })

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

        storeRef.store.dispatch(api1.endpoints.query1.initiate('in1'))

        storeRef.store.dispatch(api1.endpoints.query2.initiate('in2'))

        enhanced.enhanceEndpoints({
          endpoints: {
            query1: {
              // returned `enhanced` api contains "new" entityType
              providesTags: ['new'],
            },
            query2: {
              // @ts-expect-error
              providesTags: ['missing'],
            },
          },
        })
      })

      test('modify', () => {
        const storeRef = setupApiStore(api1, undefined, {
          withoutTestLifecycles: true,
        })

        api1.enhanceEndpoints({
          endpoints: {
            query1: {
              query: (x) => {
                expectTypeOf(x).toEqualTypeOf<'in1'>()

                return 'modified1'
              },
            },
            query2(definition) {
              definition.query = (x) => {
                expectTypeOf(x).toEqualTypeOf<'in2'>()

                return 'modified2'
              }
            },
            mutation1: {
              query: (x) => {
                expectTypeOf(x).toEqualTypeOf<'in1'>()

                return 'modified1'
              },
            },
            mutation2(definition) {
              definition.query = (x) => {
                expectTypeOf(x).toEqualTypeOf<'in2'>()

                return 'modified2'
              }
            },
            // @ts-expect-error
            nonExisting: {},
          },
        })

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

      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 api1>

        type TagTypes = TagTypesFromApi<typeof api1>

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

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

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

        const enhancedApi = baseApi.enhanceEndpoints<
          TagTypes,
          UpdatedDefinitions
        >({
          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(),
        )

        expectTypeOf(queryResponse.data).toMatchTypeOf<
          Transformed | undefined
        >()

        const mutationResponse = await storeRef.store.dispatch(
          enhancedApi.endpoints.mutation1.initiate(),
        )

        expectTypeOf(mutationResponse).toMatchTypeOf<
          | { data: Transformed }
          | { error: FetchBaseQueryError | SerializedError }
        >()
      })
    })
    describe('endpoint schemas', () => {
      const argSchema = v.object({ id: v.number() })
      const postSchema = v.object({
        id: v.number(),
        title: v.string(),
        body: v.string(),
      }) satisfies v.GenericSchema<Post>
      const errorResponseSchema = v.object({
        status: v.number(),
        data: v.unknown(),
      }) satisfies v.GenericSchema<FetchBaseQueryError>
      const metaSchema = v.object({
        request: v.instance(Request),
        response: v.optional(v.instance(Response)),
      }) satisfies v.GenericSchema<FetchBaseQueryMeta>
      test('schemas must match', () => {
        createApi({
          baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
          endpoints: (build) => ({
            query: build.query<Post, { id: number }>({
              query: ({ id }) => `/post/${id}`,
              argSchema,
              responseSchema: postSchema,
              errorResponseSchema,
              metaSchema,
            }),
            bothMismatch: build.query<Post, { id: number }>({
              query: ({ id }) => `/post/${id}`,
              // @ts-expect-error wrong schema
              argSchema: v.object({ id: v.string() }),
              // @ts-expect-error wrong schema
              responseSchema: v.object({ id: v.string() }),
              // @ts-expect-error wrong schema
              errorResponseSchema: v.object({ status: v.string() }),
              // @ts-expect-error wrong schema
              metaSchema: v.object({ request: v.string() }),
            }),
            inputMismatch: build.query<Post, { id: number }>({
              query: ({ id }) => `/post/${id}`,
              // @ts-expect-error can't expect different input
              argSchema: v.object({
                id: v.pipe(v.string(), v.transform(Number), v.number()),
              }),
              // @ts-expect-error can't expect different input
              responseSchema: v.object({
                ...postSchema.entries,
                id: v.pipe(v.string(), v.transform(Number)),
              }) satisfies v.GenericSchema<any, Post>,
              // @ts-expect-error can't expect different input
              errorResponseSchema: v.object({
                ...errorResponseSchema.entries,
                status: v.pipe(v.string(), v.transform(Number)),
              }) satisfies v.GenericSchema<any, FetchBaseQueryError>,
              // @ts-expect-error can't expect different input
              metaSchema: v.object({
                ...metaSchema.entries,
                request: v.pipe(
                  v.string(),
                  v.transform((url) => new Request(url)),
                ),
              }) satisfies v.GenericSchema<any, FetchBaseQueryMeta>,
            }),
            outputMismatch: build.query<Post, { id: number }>({
              query: ({ id }) => `/post/${id}`,
              // @ts-expect-error can't provide different output
              argSchema: v.object({
                id: v.pipe(v.number(), v.transform(String)),
              }),
              // @ts-expect-error can't provide different output
              responseSchema: v.object({
                ...postSchema.entries,
                id: v.pipe(v.number(), v.transform(String)),
              }) satisfies v.GenericSchema<Post, any>,
              // @ts-expect-error can't provide different output
              errorResponseSchema: v.object({
                ...errorResponseSchema.entries,
                status: v.pipe(v.number(), v.transform(String)),
              }) satisfies v.GenericSchema<FetchBaseQueryError, any>,
              // @ts-expect-error can't provide different output
              metaSchema: v.object({
                ...metaSchema.entries,
                request: v.pipe(
                  v.instance(Request),
                  v.transform((r) => r.url),
                ),
              }) satisfies v.GenericSchema<FetchBaseQueryMeta, any>,
            }),
          }),
        })
      })
      test('schemas as a source of inference', () => {
        const postAdapter = createEntityAdapter<Post>()
        const api = createApi({
          baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
          endpoints: (build) => ({
            query: build.query({
              query: ({ id }: { id: number }) => `/post/${id}`,
              responseSchema: postSchema,
            }),
            query2: build.query({
              query: (arg) => {
                expectTypeOf(arg).toEqualTypeOf<{ id: number }>()
                return `/post/${arg.id}`
              },
              argSchema,
              responseSchema: postSchema,
            }),
            query3: build.query({
              query: (_arg: void) => `/posts`,
              rawResponseSchema: v.array(postSchema),
              transformResponse: (posts) => {
                expectTypeOf(posts).toEqualTypeOf<Post[]>()
                return postAdapter.getInitialState(undefined, posts)
              },
            }),
          }),
        })

        expectTypeOf(api.endpoints.query.Types.QueryArg).toEqualTypeOf<{
          id: number
        }>()
        expectTypeOf(api.endpoints.query.Types.ResultType).toEqualTypeOf<Post>()

        expectTypeOf(api.endpoints.query2.Types.QueryArg).toEqualTypeOf<{
          id: number
        }>()
        expectTypeOf(
          api.endpoints.query2.Types.ResultType,
        ).toEqualTypeOf<Post>()

        expectTypeOf(api.endpoints.query3.Types.QueryArg).toEqualTypeOf<void>()
        expectTypeOf(api.endpoints.query3.Types.ResultType).toEqualTypeOf<
          EntityState<Post, Post['id']>
        >()
      })
    })
  })
})
