import type { SerializedError } from '@internal/createAsyncThunk'
import { createAsyncThunk } from '@internal/createAsyncThunk'
import { executeReducerBuilderCallback } from '@internal/mapBuilders'
import type { UnknownAction } from '@reduxjs/toolkit'
import { createAction } from '@reduxjs/toolkit'

describe('type tests', () => {
  test('builder callback for actionMap', () => {
    const increment = createAction<number, 'increment'>('increment')

    const decrement = createAction<number, 'decrement'>('decrement')

    executeReducerBuilderCallback<number>((builder) => {
      builder.addCase(increment, (state, action) => {
        expectTypeOf(state).toBeNumber()

        expectTypeOf(action).toEqualTypeOf<{
          type: 'increment'
          payload: number
        }>()

        expectTypeOf(state).not.toBeString()

        expectTypeOf(action).not.toMatchTypeOf<{
          type: 'increment'
          payload: string
        }>()

        expectTypeOf(action).not.toMatchTypeOf<{
          type: 'decrement'
          payload: number
        }>()
      })

      builder.addCase('increment', (state, action) => {
        expectTypeOf(state).toBeNumber()

        expectTypeOf(action).toEqualTypeOf<{ type: 'increment' }>()

        expectTypeOf(state).not.toBeString()

        expectTypeOf(action).not.toMatchTypeOf<{ type: 'decrement' }>()

        // this cannot be inferred and has to be manually specified
        expectTypeOf(action).not.toMatchTypeOf<{
          type: 'increment'
          payload: number
        }>()
      })

      builder.addCase(
        increment,
        (state, action: ReturnType<typeof increment>) => state,
      )

      // @ts-expect-error
      builder.addCase(
        increment,
        (state, action: ReturnType<typeof decrement>) => state,
      )

      builder.addCase(
        'increment',
        (state, action: ReturnType<typeof increment>) => state,
      )

      // @ts-expect-error
      builder.addCase(
        'decrement',
        (state, action: ReturnType<typeof increment>) => state,
      )

      // action type is inferred
      builder.addMatcher(increment.match, (state, action) => {
        expectTypeOf(action).toEqualTypeOf<ReturnType<typeof increment>>()
      })

      test('action type is inferred when type predicate lacks `type` property', () => {
        type PredicateWithoutTypeProperty = {
          payload: number
        }

        builder.addMatcher(
          (action): action is PredicateWithoutTypeProperty => true,
          (state, action) => {
            expectTypeOf(action).toMatchTypeOf<PredicateWithoutTypeProperty>()

            expectTypeOf(action).toMatchTypeOf<UnknownAction>()
          },
        )
      })

      // action type defaults to UnknownAction if no type predicate matcher is passed
      builder.addMatcher(
        () => true,
        (state, action) => {
          expectTypeOf(action).toMatchTypeOf<UnknownAction>()
        },
      )

      // with a boolean checker, action can also be typed by type argument
      builder.addMatcher<{ foo: boolean }>(
        () => true,
        (state, action) => {
          expectTypeOf(action).toMatchTypeOf<{ foo: boolean }>()

          expectTypeOf(action).toMatchTypeOf<UnknownAction>()
        },
      )

      // addCase().addMatcher() is possible, action type inferred correctly
      builder
        .addCase(
          'increment',
          (state, action: ReturnType<typeof increment>) => state,
        )
        .addMatcher(decrement.match, (state, action) => {
          expectTypeOf(action).toEqualTypeOf<ReturnType<typeof decrement>>()
        })

      // addCase().addDefaultCase() is possible, action type is UnknownAction
      builder
        .addCase(
          'increment',
          (state, action: ReturnType<typeof increment>) => state,
        )
        .addDefaultCase((state, action) => {
          expectTypeOf(action).toMatchTypeOf<UnknownAction>()
        })

      test('addMatcher() should prevent further calls to addCase()', () => {
        const b = builder.addMatcher(increment.match, () => {})

        expectTypeOf(b).not.toHaveProperty('addCase')

        expectTypeOf(b.addMatcher).toBeCallableWith(increment.match, () => {})

        expectTypeOf(b.addDefaultCase).toBeCallableWith(() => {})
      })

      test('addDefaultCase() should prevent further calls to addCase(), addMatcher() and addDefaultCase', () => {
        const b = builder.addDefaultCase(() => {})

        expectTypeOf(b).not.toHaveProperty('addCase')

        expectTypeOf(b).not.toHaveProperty('addMatcher')

        expectTypeOf(b).not.toHaveProperty('addDefaultCase')
      })

      describe('`createAsyncThunk` actions work with `mapBuilder`', () => {
        test('case 1: normal `createAsyncThunk`', () => {
          const thunk = createAsyncThunk('test', () => {
            return 'ret' as const
          })
          builder.addCase(thunk.pending, (_, action) => {
            expectTypeOf(action).toMatchTypeOf<{
              payload: undefined
              meta: {
                arg: void
                requestId: string
                requestStatus: 'pending'
              }
            }>()
          })

          builder.addCase(thunk.rejected, (_, action) => {
            expectTypeOf(action).toMatchTypeOf<{
              payload: unknown
              error: SerializedError
              meta: {
                arg: void
                requestId: string
                requestStatus: 'rejected'
                aborted: boolean
                condition: boolean
                rejectedWithValue: boolean
              }
            }>()
          })
          builder.addCase(thunk.fulfilled, (_, action) => {
            expectTypeOf(action).toMatchTypeOf<{
              payload: 'ret'
              meta: {
                arg: void
                requestId: string
                requestStatus: 'fulfilled'
              }
            }>()
          })
        })
      })

      test('case 2: `createAsyncThunk` with `meta`', () => {
        const thunk = createAsyncThunk<
          'ret',
          void,
          {
            pendingMeta: { startedTimeStamp: number }
            fulfilledMeta: {
              fulfilledTimeStamp: number
              baseQueryMeta: 'meta!'
            }
            rejectedMeta: {
              baseQueryMeta: 'meta!'
            }
          }
        >(
          'test',
          (_, api) => {
            return api.fulfillWithValue('ret' as const, {
              fulfilledTimeStamp: 5,
              baseQueryMeta: 'meta!',
            })
          },
          {
            getPendingMeta() {
              return { startedTimeStamp: 0 }
            },
          },
        )

        builder.addCase(thunk.pending, (_, action) => {
          expectTypeOf(action).toMatchTypeOf<{
            payload: undefined
            meta: {
              arg: void
              requestId: string
              requestStatus: 'pending'
              startedTimeStamp: number
            }
          }>()
        })

        builder.addCase(thunk.rejected, (_, action) => {
          expectTypeOf(action).toMatchTypeOf<{
            payload: unknown
            error: SerializedError
            meta: {
              arg: void
              requestId: string
              requestStatus: 'rejected'
              aborted: boolean
              condition: boolean
              rejectedWithValue: boolean
              baseQueryMeta?: 'meta!'
            }
          }>()

          if (action.meta.rejectedWithValue) {
            expectTypeOf(action.meta.baseQueryMeta).toEqualTypeOf<'meta!'>()
          }
        })
        builder.addCase(thunk.fulfilled, (_, action) => {
          expectTypeOf(action).toMatchTypeOf<{
            payload: 'ret'
            meta: {
              arg: void
              requestId: string
              requestStatus: 'fulfilled'
              baseQueryMeta: 'meta!'
            }
          }>()
        })
      })
    })
  })
})
