import { IncomingHttpHeaders } from 'node:http'
import { FromSchema, JSONSchema } from 'json-schema-to-ts'
import { Type, TSchema, Static } from 'typebox'
import { expect } from 'tstyche'
import fastify, {
  FastifyTypeProvider,
  HookHandlerDoneFunction,
  FastifyRequest,
  FastifyReply,
  FastifyInstance,
  FastifyError,
  SafePromiseLike
} from '../../fastify.js'

const server = fastify()

// -------------------------------------------------------------------
// Default (unknown)
// -------------------------------------------------------------------

server.get('/', (req) => expect(req.body).type.toBe<unknown>())

// -------------------------------------------------------------------
// Remapping
// -------------------------------------------------------------------

interface NumberProvider extends FastifyTypeProvider {
  validator: number
  serializer: number
} // remap all schemas to numbers

server.withTypeProvider<NumberProvider>().get(
  '/',
  {
    schema: {
      body: { type: 'string' },
      querystring: { type: 'string' },
      headers: { type: 'string' },
      params: { type: 'string' }
    }
  },
  (req) => {
    expect(req.headers).type.toBe<number & IncomingHttpHeaders>()
    expect(req.body).type.toBe<number>()
    expect(req.query).type.toBe<number>()
    expect(req.params).type.toBe<number>()
  }
)

// -------------------------------------------------------------------
// Override
// -------------------------------------------------------------------

interface OverriddenProvider extends FastifyTypeProvider { validator: 'inferenced' }

server.withTypeProvider<OverriddenProvider>().get<{ Body: 'override' }>(
  '/',
  {
    schema: {
      body: Type.Object({
        x: Type.Number(),
        y: Type.Number(),
        z: Type.Number()
      })
    }
  },
  (req) => {
    expect(req.body).type.toBe<'override'>()
  }
)

// -------------------------------------------------------------------
// TypeBox
// -------------------------------------------------------------------

interface TypeBoxProvider extends FastifyTypeProvider {
  validator: this['schema'] extends TSchema ? Static<this['schema']> : unknown
  serializer: this['schema'] extends TSchema ? Static<this['schema']> : unknown
}

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      body: Type.Object({
        x: Type.Number(),
        y: Type.Number(),
        z: Type.Number()
      })
    },
    errorHandler: (error, request, reply) => {
      expect(error).type.toBe<FastifyError>()
      expect(request).type.toBeAssignableTo<FastifyRequest>()
      expect(request.body.x).type.toBe<number>()
      expect(request.body.y).type.toBe<number>()
      expect(request.body.z).type.toBe<number>()
      expect(reply).type.toBeAssignableTo<FastifyReply>()
    }
  },
  (req) => {
    expect(req.body.x).type.toBe<number>()
    expect(req.body.y).type.toBe<number>()
    expect(req.body.z).type.toBe<number>()
  }
)

expect(server.withTypeProvider<TypeBoxProvider>()).type.toBeAssignableTo<FastifyInstance>()

// -------------------------------------------------------------------
// JsonSchemaToTs
// -------------------------------------------------------------------

interface JsonSchemaToTsProvider extends FastifyTypeProvider {
  validator: this['schema'] extends JSONSchema ? FromSchema<this['schema']> : unknown
  serializer: this['schema'] extends JSONSchema ? FromSchema<this['schema']> : unknown
}

// explicitly setting schema `as const`

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      body: {
        type: 'object',
        properties: {
          x: { type: 'number' },
          y: { type: 'string' },
          z: { type: 'boolean' }
        }
      } as const
    },
    errorHandler: (error, request, reply) => {
      expect(error).type.toBe<FastifyError>()
      expect(request).type.toBeAssignableTo<FastifyRequest>()
      expect(request.body.x).type.toBe<number | undefined>()
      expect(request.body.y).type.toBe<string | undefined>()
      expect(request.body.z).type.toBe<boolean | undefined>()
      expect(reply).type.toBeAssignableTo<FastifyReply>()
    }
  },
  (req) => {
    expect(req.body.x).type.toBe<number | undefined>()
    expect(req.body.y).type.toBe<string | undefined>()
    expect(req.body.z).type.toBe<boolean | undefined>()
  }
)

server.withTypeProvider<JsonSchemaToTsProvider>().route({
  url: '/',
  method: 'POST',
  schema: {
    body: {
      type: 'object',
      properties: {
        x: { type: 'number' },
        y: { type: 'string' },
        z: { type: 'boolean' }
      }
    }
  } as const,
  errorHandler: (error, request, reply) => {
    expect(error).type.toBe<FastifyError>()
    expect(request).type.toBeAssignableTo<FastifyRequest>()
    expect(request.body.x).type.toBe<number | undefined>()
    expect(request.body.y).type.toBe<string | undefined>()
    expect(request.body.z).type.toBe<boolean | undefined>()
    expect(reply).type.toBeAssignableTo<FastifyReply>()
  },
  handler: (req) => {
    expect(req.body.x).type.toBe<number | undefined>()
    expect(req.body.y).type.toBe<string | undefined>()
    expect(req.body.z).type.toBe<boolean | undefined>()
  }
})

// inferring schema `as const`

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      body: {
        type: 'object',
        properties: {
          x: { type: 'number' },
          y: { type: 'string' },
          z: { type: 'boolean' }
        }
      }
    },
    errorHandler: (error, request, reply) => {
      expect(error).type.toBe<FastifyError>()
      expect(request).type.toBeAssignableTo<FastifyRequest>()
      expect(request.body.x).type.toBe<number | undefined>()
      expect(request.body.y).type.toBe<string | undefined>()
      expect(request.body.z).type.toBe<boolean | undefined>()
      expect(reply).type.toBeAssignableTo<FastifyReply>()
    }
  },
  (req) => {
    expect(req.body.x).type.toBe<number | undefined>()
    expect(req.body.y).type.toBe<string | undefined>()
    expect(req.body.z).type.toBe<boolean | undefined>()
  }
)

server.withTypeProvider<JsonSchemaToTsProvider>().route({
  url: '/',
  method: 'POST',
  schema: {
    body: {
      type: 'object',
      properties: {
        x: { type: 'number' },
        y: { type: 'string' },
        z: { type: 'boolean' }
      }
    }
  },
  errorHandler: (error, request, reply) => {
    expect(error).type.toBe<FastifyError>()
    expect(request).type.toBeAssignableTo<FastifyRequest>()
    expect(request.body.x).type.toBe<number | undefined>()
    expect(request.body.y).type.toBe<string | undefined>()
    expect(request.body.z).type.toBe<boolean | undefined>()
    expect(reply).type.toBeAssignableTo<FastifyReply>()
  },
  handler: (req) => {
    expect(req.body.x).type.toBe<number | undefined>()
    expect(req.body.y).type.toBe<string | undefined>()
    expect(req.body.z).type.toBe<boolean | undefined>()
  }
})

expect(server.withTypeProvider<JsonSchemaToTsProvider>()).type.toBeAssignableTo<FastifyInstance>()

// -------------------------------------------------------------------
// Instance Type Remappable
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      body: {
        type: 'object',
        properties: {
          x: { type: 'number' },
          y: { type: 'string' },
          z: { type: 'boolean' }
        }
      } as const
    },
    errorHandler: (error, request, reply) => {
      expect(error).type.toBe<FastifyError>()
      expect(request).type.toBeAssignableTo<FastifyRequest>()
      expect(request.body.x).type.toBe<number | undefined>()
      expect(request.body.y).type.toBe<string | undefined>()
      expect(request.body.z).type.toBe<boolean | undefined>()
      expect(reply).type.toBeAssignableTo<FastifyReply>()
    }
  },
  (req) => {
    expect(req.body.x).type.toBe<number | undefined>()
    expect(req.body.y).type.toBe<string | undefined>()
    expect(req.body.z).type.toBe<boolean | undefined>()
  }
)

// -------------------------------------------------------------------
// Request Hooks
// -------------------------------------------------------------------

// Sync handlers

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      body: Type.Object({
        x: Type.Number(),
        y: Type.String(),
        z: Type.Boolean()
      })
    },
    preHandler: (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preParsing: (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preSerialization: (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preValidation: (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onError: (req, reply, error, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onRequest: (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onResponse: (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onTimeout: (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onSend: (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    }
  },
  req => {
    expect(req.body.x).type.toBe<number>()
    expect(req.body.y).type.toBe<string>()
    expect(req.body.z).type.toBe<boolean>()
  }
)

// Async handlers

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      body: Type.Object({
        x: Type.Number(),
        y: Type.String(),
        z: Type.Boolean()
      })
    },
    preHandler: async (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preParsing: async (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preSerialization: async (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    preValidation: async (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onError: async (req, reply, error, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onRequest: async (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onResponse: async (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onTimeout: async (req, reply, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    },
    onSend: async (req, reply, payload, done) => {
      expect(req.body.x).type.toBe<number>()
      expect(req.body.y).type.toBe<string>()
      expect(req.body.z).type.toBe<boolean>()
    }
  },
  req => {
    expect(req.body.x).type.toBe<number>()
    expect(req.body.y).type.toBe<string>()
    expect(req.body.z).type.toBe<boolean>()
  }
)

// -------------------------------------------------------------------
// Request headers
// -------------------------------------------------------------------

// JsonSchemaToTsProvider
server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      headers: {
        type: 'object',
        properties: {
          lowercase: { type: 'string' },
          UPPERCASE: { type: 'number' },
          camelCase: { type: 'boolean' },
          'KEBAB-case': { type: 'boolean' },
          PRESERVE_OPTIONAL: { type: 'number' }
        },
        required: ['lowercase', 'UPPERCASE', 'camelCase', 'KEBAB-case']
      } as const
    }
  },
  (req) => {
    expect(req.headers.lowercase).type.toBe<string>()
    expect(req.headers.UPPERCASE).type.toBe<string | string[] | undefined>()
    expect(req.headers.uppercase).type.toBe<number>()
    expect(req.headers.camelcase).type.toBe<boolean>()
    expect(req.headers['kebab-case']).type.toBe<boolean>()
    expect(req.headers.preserve_optional).type.toBe<number | undefined>()
  }
)

// TypeBoxProvider
server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      headers: Type.Object({
        lowercase: Type.String(),
        UPPERCASE: Type.Number(),
        camelCase: Type.Boolean(),
        'KEBAB-case': Type.Boolean(),
        PRESERVE_OPTIONAL: Type.Optional(Type.Number())
      })
    }
  },
  (req) => {
    expect(req.headers.lowercase).type.toBe<string>()
    expect(req.headers.UPPERCASE).type.toBe<string | string[] | undefined>()
    expect(req.headers.uppercase).type.toBe<number>()
    expect(req.headers.camelcase).type.toBe<boolean>()
    expect(req.headers['kebab-case']).type.toBe<boolean>()
    expect(req.headers.preserve_optional).type.toBe<number | undefined>()
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Type
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: Type.String(),
        400: Type.Number(),
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    res.send('hello')
    res.send(42)
    res.send({ error: 'error' })
    expect(res.code(200).send).type.toBe<((...args: [payload: string]) => typeof res)>()
    expect(res.code(400).send).type.toBe<((...args: [payload: number]) => typeof res)>()
    expect(res.code(500).send).type.toBe<((...args: [payload: { error: string }]) => typeof res)>()
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Type (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: Type.String()
            },
            'application/json': {
              schema: Type.Object({
                msg: Type.String()
              })
            }
          }
        },
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    res.send('hello')
    res.send({ msg: 'hello' })
    res.send({ error: 'error' })
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Type: Non Assignable
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: Type.String(),
        400: Type.Number(),
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    expect(res.send).type.not.toBeCallableWith(false)
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Type: Non Assignable (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: Type.String()
            },
            'application/json': {
              schema: Type.Object({
                msg: Type.String()
              })
            }
          }
        },
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    expect(res.send).type.not.toBeCallableWith(false)
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Return Type
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: Type.String(),
        400: Type.Number(),
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2 | 3
    switch (option) {
      case 1: return 'hello'
      case 2: return 42
      case 3: return { error: 'error' }
    }
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Return Type (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: Type.String()
            },
            'application/json': {
              schema: Type.Object({
                msg: Type.String()
              })
            }
          }
        },
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2 | 3
    switch (option) {
      case 1: return 'hello'
      case 2: return { msg: 'hello' }
      case 3: return { error: 'error' }
    }
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Return Type: Non Assignable
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: Type.String(),
        400: Type.Number(),
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  // @ts-expect-error  Type 'boolean' is not assignable to type 'string | number | void | { error: string; }'.
  async (_, res) => {
    return false
  }
)

// -------------------------------------------------------------------
// TypeBox Reply Return Type: Non Assignable (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<TypeBoxProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: Type.String()
            },
            'application/json': {
              schema: Type.Object({
                msg: Type.String()
              })
            }
          }
        },
        500: Type.Object({
          error: Type.String()
        })
      }
    }
  },
  // @ts-expect-error  Type 'boolean' is not assignable to type 'string | void | { msg: string; } | { error: string; }'.
  async (_, res) => {
    return false
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  (_, res) => {
    res.send('hello')
    res.send(42)
    res.send({ error: 'error' })
    expect(res.code(200).send).type.toBe<((...args: [payload: string]) => typeof res)>()
    expect(res.code(400).send).type.toBe<((...args: [payload: number]) => typeof res)>()
    expect(res.code(500).send).type.toBe<
      ((...args: [payload: { [x: string]: unknown; error?: string }]) => typeof res)
    >()
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  (_, res) => {
    res.send('hello')
    res.send({ msg: 'hello' })
    res.send({ error: 'error' })
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type: Non Assignable
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    expect(res.send).type.not.toBeCallableWith(false)
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type: Non Assignable (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    expect(res.send).type.not.toBeCallableWith(false)
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type Return
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2 | 3
    switch (option) {
      case 1: return 'hello'
      case 2: return 42
      case 3: return { error: 'error' }
    }
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type Return (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    const option = 1 as 1 | 2 | 3
    switch (option) {
      case 1: return 'hello'
      case 2: return { msg: 'hello' }
      case 3: return { error: 'error' }
    }
  }
)

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type Return: Non Assignable
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  // @ts-expect-error  Type 'boolean' is not assignable to type 'string | number | void | { [x: string]: unknown; error?: string | undefined; }'.
  async (_, res) => {
    return false
  }
)

// https://github.com/fastify/fastify/issues/4088
server.withTypeProvider<JsonSchemaToTsProvider>().get('/', {
  schema: {
    response: {
      200: { type: 'string' }
    }
  } as const
  // @ts-expect-error  Type '{ foo: number; }' is not assignable to type 'string | void | Promise<string | void>'.
}, (_, res) => {
  return { foo: 555 }
})

// -------------------------------------------------------------------
// JsonSchemaToTs Reply Type Return: Non Assignable (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  // @ts-expect-error  Type 'Promise<boolean>' is not assignable to type 'string | void | { [x: string]: unknown; msg?: string | undefined; } | { [x: string]: unknown; error?: string | undefined; } | Promise<string | void | { [x: string]: unknown; msg?: string | undefined; } | { [x: string]: unknown; error?: string | undefined; }>'.
  async (_, res) => {
    return false
  }
)

// -------------------------------------------------------------------
// Reply Type Override
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    res.send(true)
  }
)

// -------------------------------------------------------------------
// Reply Type Override (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    res.send(true)
  }
)

// -------------------------------------------------------------------
// Reply Type Return Override
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>(
  '/',
  {
    schema: {
      response: {
        200: { type: 'string' },
        400: { type: 'number' },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    return true
  }
)

// -------------------------------------------------------------------
// Reply Type Return Override (Different Content-types)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get<{ Reply: boolean }>(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    return true
  }
)

// -------------------------------------------------------------------
// Reply Status Code (Different Status Codes)
// -------------------------------------------------------------------

server.withTypeProvider<JsonSchemaToTsProvider>().get(
  '/',
  {
    schema: {
      response: {
        200: {
          content: {
            'text/string': {
              schema: { type: 'string' }
            },
            'application/json': {
              schema: { type: 'object', properties: { msg: { type: 'string' } } }
            }
          }
        },
        500: { type: 'object', properties: { error: { type: 'string' } } }
      } as const
    }
  },
  async (_, res) => {
    res.code(200)
    res.code(500)
    expect(res.code).type.not.toBeCallableWith(201)
    expect(res.code).type.not.toBeCallableWith(400)
  }
)

// -------------------------------------------------------------------
// RouteGeneric Reply Type Return (Different Status Codes)
// -------------------------------------------------------------------

server.get<{
  Reply: {
    200: string | { msg: string }
    400: number
    '5xx': { error: string }
  }
}>(
  '/',
  async (_, res) => {
    const option = 1 as 1 | 2 | 3 | 4
    switch (option) {
      case 1: return 'hello'
      case 2: return { msg: 'hello' }
      case 3: return 400
      case 4: return { error: 'error' }
    }
  }
)

// -------------------------------------------------------------------
// RouteGeneric Status Code (Different Status Codes)
// -------------------------------------------------------------------

server.get<{
  Reply: {
    200: string | { msg: string }
    400: number
    '5xx': { error: string }
  }
}>(
  '/',
  async (_, res) => {
    res.code(200)
    res.code(400)
    res.code(500)
    res.code(502)
    expect(res.code).type.not.toBeCallableWith(201)
    expect(res.code).type.not.toBeCallableWith(300)
    expect(res.code).type.not.toBeCallableWith(404)
    return 'hello'
  }
)

// -------------------------------------------------------------------
// RouteGeneric Reply Type Return: Non Assignable (Different Status Codes)
// -------------------------------------------------------------------

server.get<{
  Reply: {
    200: string | { msg: string }
    400: number
    '5xx': { error: string }
  }
}>(
  '/',
  // @ts-expect-error  Type 'boolean' is not assignable to type 'string | number | void | { msg: string; } | { error: string; } | { 200: string | { msg: string; }; 400: number; '5xx': { error: string; }; }'.
  async (_, res) => {
    return true
  }
)

// -------------------------------------------------------------------
// FastifyPlugin: Auxiliary
// -------------------------------------------------------------------

interface AuxiliaryPluginProvider extends FastifyTypeProvider { validator: 'plugin-auxiliary' }

// Auxiliary plugins may have varying server types per application. Recommendation would be to explicitly remap instance provider context within plugin if required.
function plugin<T extends FastifyInstance> (instance: T) {
  instance.withTypeProvider<AuxiliaryPluginProvider>().get(
    '/',
    {
      schema: { body: null }
    },
    (req) => {
      expect(req.body).type.toBe<'plugin-auxiliary'>()
    }
  )
}

server.withTypeProvider<AuxiliaryPluginProvider>().register(plugin).get(
  '/',
  {
    schema: { body: null }
  },
  (req) => {
    expect(req.body).type.toBe<'plugin-auxiliary'>()
  }
)

// -------------------------------------------------------------------
// Handlers: Inline
// -------------------------------------------------------------------

interface InlineHandlerProvider extends FastifyTypeProvider { validator: 'handler-inline' }

// Inline handlers should infer for the request parameters (non-shared)
server.withTypeProvider<InlineHandlerProvider>().get(
  '/',
  {
    onRequest: (req, res, done) => {
      expect(req.body).type.toBe<'handler-inline'>()
    },
    schema: { body: null }
  },
  (req) => {
    expect(req.body).type.toBe<'handler-inline'>()
  }
)

// -------------------------------------------------------------------
// Handlers: Auxiliary
// -------------------------------------------------------------------

interface AuxiliaryHandlerProvider extends FastifyTypeProvider { validator: 'handler-auxiliary' }

// Auxiliary handlers are likely shared for multiple routes and thus should infer as unknown due to potential varying parameters
function auxiliaryHandler (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction): void {
  expect(request.body).type.toBe<unknown>()
}

server.withTypeProvider<AuxiliaryHandlerProvider>().get(
  '/',
  {
    onRequest: auxiliaryHandler,
    schema: { body: 'handler-auxiliary' }
  },
  (req) => {
    expect(req.body).type.toBe<'handler-auxiliary'>()
  }
)

// -------------------------------------------------------------------
// SafePromiseLike
// -------------------------------------------------------------------
const safePromiseLike = {
  then: new Promise<string>(resolve => resolve('')).then,
  __linterBrands: 'SafePromiseLike' as const
}
expect(safePromiseLike).type.toBeAssignableTo<SafePromiseLike<string>>()
expect(safePromiseLike).type.toBeAssignableTo<PromiseLike<string>>()
expect(safePromiseLike).type.not.toBeAssignableTo<Promise<string>>()

// -------------------------------------------------------------------
// Separate Providers
// -------------------------------------------------------------------

interface SeparateProvider extends FastifyTypeProvider {
  validator: string
  serializer: Date
}

server.withTypeProvider<SeparateProvider>().get(
  '/',
  {
    schema: {
      body: null,
      response: {
        200: { type: 'string' }
      }
    }
  },
  (req, res) => {
    expect(req.body).type.toBe<string>()

    res.send(new Date())
  }
)
