import { AddressInfo } from 'node:net'
import { ConstraintStrategy, Config as FindMyWayConfig } from 'find-my-way'
import { expect } from 'tstyche'
import fastify, {
  FastifyBaseLogger,
  FastifyBodyParser,
  FastifyError,
  FastifyInstance,
  FastifyListenOptions,
  FastifyRouterOptions,
  RawReplyDefaultExpression,
  RawRequestDefaultExpression,
  RawServerDefault,
  RouteGenericInterface
} from '../../fastify.js'
import { HookHandlerDoneFunction } from '../../types/hooks.js'
import { FindMyWayVersion } from '../../types/instance.js'
import { Bindings, ChildLoggerOptions } from '../../types/logger.js'
import { FastifyReply } from '../../types/reply.js'
import { FastifyRequest } from '../../types/request.js'
import { FastifySchemaCompiler, FastifySchemaControllerOptions, FastifySerializerCompiler } from '../../types/schema.js'

const server = fastify()

expect(server.addSchema({ type: 'null' })).type.toBe<FastifyInstance>()
expect(server.addSchema({ schemaId: 'id' })).type.toBe<FastifyInstance>()
expect(server.addSchema({ schemas: [] })).type.toBe<FastifyInstance>()

expect(server.pluginName).type.toBe<string>()

expect(server.getSchemas()).type.toBe<Record<string, unknown>>()
expect(server.addresses()).type.toBe<AddressInfo[]>()
expect(server.getSchema('SchemaId')).type.toBe<unknown>()
expect(server.printRoutes()).type.toBe<string>()
expect(server.printPlugins()).type.toBe<string>()
expect(server.listeningOrigin).type.toBe<string>()
expect(server.supportedMethods).type.toBe<string[]>()

expect(
  server.setErrorHandler(function (error, request, reply) {
    expect(error).type.toBe<unknown>()
    expect(this).type.toBe<FastifyInstance>()
  })
).type.toBe<FastifyInstance>()

expect(
  server.setErrorHandler<FastifyError>(function (error, request, reply) {
    expect(error).type.toBe<FastifyError>()
  })
).type.toBe<FastifyInstance>()

expect(
  server.setGenReqId(function (req) {
    expect(req).type.toBe<RawRequestDefaultExpression>()
    return 'foo'
  })
).type.toBe<FastifyInstance>()

function fastifySetGenReqId (req: RawRequestDefaultExpression) {
  return 'foo'
}
server.setGenReqId(fastifySetGenReqId)

function fastifyErrorHandler (this: FastifyInstance, error: FastifyError) { }
server.setErrorHandler(fastifyErrorHandler)

async function asyncFastifyErrorHandler (this: FastifyInstance, error: FastifyError) { }
server.setErrorHandler(asyncFastifyErrorHandler)

function nodeJSErrorHandler (error: NodeJS.ErrnoException) {
  if (error) { throw error }
}
server.setErrorHandler(nodeJSErrorHandler)

function asyncNodeJSErrorHandler (error: NodeJS.ErrnoException) {
  if (error) { throw error }
}
server.setErrorHandler(asyncNodeJSErrorHandler)

class CustomError extends Error {
  private __brand: any
}
interface ReplyPayload {
  Reply: {
    test: boolean;
  };
}
// typed sync error handler
server.setErrorHandler<CustomError, ReplyPayload>((error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  expect(reply.send).type.toBe<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>()
})
// typed async error handler send
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  expect(reply.send).type.toBe<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>()
})
// typed async error handler return
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  return { test: true }
})
// typed sync error handler send error
server.setErrorHandler<CustomError, ReplyPayload>((error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  // @ts-expect-error  Type 'string' is not assignable to type 'boolean'.
  reply.send({ test: 'foo' })
})
// typed sync error handler return error
server.setErrorHandler<CustomError, ReplyPayload>((error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  return { test: 'foo' }
})
// typed async error handler send error
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  // @ts-expect-error  Type 'string' is not assignable to type 'boolean'.
  reply.send({ test: 'foo' })
})
// typed async error handler return error
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
  expect(error).type.toBe<CustomError>()
  return { test: 'foo' }
})

function notFoundHandler (request: FastifyRequest, reply: FastifyReply) { }
async function notFoundAsyncHandler (request: FastifyRequest, reply: FastifyReply) { }
function notFoundpreHandlerHandler (
  request: FastifyRequest,
  reply: FastifyReply,
  done: HookHandlerDoneFunction
) { done() }
async function notFoundpreHandlerAsyncHandler (
  request: FastifyRequest,
  reply: FastifyReply
) { }
function notFoundpreValidationHandler (
  request: FastifyRequest,
  reply: FastifyReply,
  done: HookHandlerDoneFunction
) { done() }
async function notFoundpreValidationAsyncHandler (
  request: FastifyRequest,
  reply: FastifyReply
) { }

server.setNotFoundHandler(notFoundHandler)
server.setNotFoundHandler({ preHandler: notFoundpreHandlerHandler }, notFoundHandler)
server.setNotFoundHandler({ preHandler: notFoundpreHandlerAsyncHandler }, notFoundHandler)
server.setNotFoundHandler({ preValidation: notFoundpreValidationHandler }, notFoundHandler)
server.setNotFoundHandler({ preValidation: notFoundpreValidationAsyncHandler }, notFoundHandler)
server.setNotFoundHandler(
  { preHandler: notFoundpreHandlerHandler, preValidation: notFoundpreValidationHandler },
  notFoundHandler
)

server.setNotFoundHandler(notFoundAsyncHandler)
server.setNotFoundHandler({ preHandler: notFoundpreHandlerHandler }, notFoundAsyncHandler)
server.setNotFoundHandler({ preHandler: notFoundpreHandlerAsyncHandler }, notFoundAsyncHandler)
server.setNotFoundHandler({ preValidation: notFoundpreValidationHandler }, notFoundAsyncHandler)
server.setNotFoundHandler({ preValidation: notFoundpreValidationAsyncHandler }, notFoundAsyncHandler)
server.setNotFoundHandler(
  { preHandler: notFoundpreHandlerHandler, preValidation: notFoundpreValidationHandler },
  notFoundAsyncHandler
)

server.setNotFoundHandler(function (_, reply) {
  return reply.send('')
})

server.setSchemaController({
  bucket: (parentSchemas) => {
    expect(parentSchemas).type.toBe<unknown>()

    return {
      add (schema) {
        expect(schema).type.toBe<unknown>()
        expect(server.addSchema({ type: 'null' })).type.toBe<FastifyInstance>()
        return server.addSchema({ type: 'null' })
      },
      getSchema (schemaId) {
        expect(schemaId).type.toBe<string>()
        return server.getSchema('SchemaId')
      },
      getSchemas () {
        expect(server.getSchemas()).type.toBe<Record<string, unknown>>()
        return server.getSchemas()
      }
    }
  }
})

function invalidSchemaController (schemaControllerOptions: FastifySchemaControllerOptions) { }
// @ts-expect-error  Type '(schemaControllerOptions: FastifySchemaControllerOptions) => void' has no properties in common with type 'FastifySchemaControllerOptions'.
server.setSchemaController(invalidSchemaController)

server.setReplySerializer(function (payload, statusCode) {
  expect(payload).type.toBe<unknown>()
  expect(statusCode).type.toBe<number>()
  return 'serialized'
})

function invalidReplySerializer (payload: number, statusCode: string) { }
// @ts-expect-error  Argument of type '(payload: number, statusCode: string) => void' is not assignable to parameter of type '(payload: unknown, statusCode: number) => string'.
server.setReplySerializer(invalidReplySerializer)

function serializerWithInvalidReturn (payload: unknown, statusCode: number) { }
// @ts-expect-error  Argument of type '(payload: unknown, statusCode: number) => void' is not assignable to parameter of type '(payload: unknown, statusCode: number) => string'.
server.setReplySerializer(serializerWithInvalidReturn)

function invalidSchemaErrorFormatter (err: Error) {
  if (err) { throw err }
}
// @ts-expect-error  Argument of type '(err: Error) => void' is not assignable to parameter of type 'SchemaErrorFormatter'.
server.setSchemaErrorFormatter(invalidSchemaErrorFormatter)

expect(server.addHttpMethod('SEARCH', { hasBody: true })).type.toBe<FastifyInstance>()
expect(server.addHttpMethod('GET', { overrideExisting: true })).type.toBe<FastifyInstance>()
expect(server.addHttpMethod('POST', { hasBody: true, overrideExisting: true })).type.toBe<FastifyInstance>()

// test listen opts objects
const options: FastifyListenOptions = {}

expect(server.listen()).type.toBe<Promise<string>>()
expect(server.listen(options)).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000 })).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000, listenTextResolver: (address) => { return `address: ${address}` } })).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000, host: '0.0.0.0' })).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000, host: '0.0.0.0', backlog: 42 })).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000, host: '0.0.0.0', backlog: 42, exclusive: true })).type.toBe<Promise<string>>()
expect(server.listen({ port: 3000, host: '::/0', ipv6Only: true })).type.toBe<Promise<string>>()

expect(server.listen(() => { })).type.toBe<void>()
expect(server.listen({ port: 3000 }, () => { })).type.toBe<void>()
expect(server.listen({ port: 3000, listenTextResolver: (address) => { return `address: ${address}` } }, () => { })).type.toBe<void>()
expect(server.listen({ port: 3000, host: '0.0.0.0' }, () => { })).type.toBe<void>()
expect(server.listen({ port: 3000, host: '0.0.0.0', backlog: 42 }, () => { })).type.toBe<void>()
expect(server.listen({ port: 3000, host: '0.0.0.0', backlog: 42, exclusive: true }, () => { })).type.toBe<void>()
expect(server.listen({ port: 3000, host: '::/0', ipv6Only: true }, () => { })).type.toBe<void>()

// test after method
expect(server.after()).type.toBeAssignableTo<FastifyInstance>()
expect(server.after((err) => {
  expect(err).type.toBe<Error | null>()
})).type.toBeAssignableTo<FastifyInstance>()

// test ready method
expect(server.ready()).type.toBeAssignableTo<FastifyInstance>()
expect(server.ready((err) => {
  expect(err).type.toBe<Error | null>()
})).type.toBeAssignableTo<FastifyInstance>()
expect(server.ready(async (err) => {
  expect(err).type.toBe<Error | null>()
})).type.toBeAssignableTo<FastifyInstance>()

expect(server.routing({} as RawRequestDefaultExpression, {} as RawReplyDefaultExpression)).type.toBe<void>()

expect(fastify().get<RouteGenericInterface, { contextKey: string }>('/', {
  handler: () => { },
  errorHandler: (error, request, reply) => {
    expect(error).type.toBe<FastifyError>()
    expect(request).type.toBeAssignableTo<FastifyRequest>()
    expect(request.routeOptions.config).type.toBeAssignableTo<{ contextKey: string }>()
    expect(reply).type.toBeAssignableTo<FastifyReply>()
    expect(server.errorHandler(error, request, reply)).type.toBe<void>()
  }
})).type.toBe<FastifyInstance>()

expect(fastify().get('/', {
  handler: () => { },
  childLoggerFactory: (logger, bindings, opts, req) => {
    expect(server.childLoggerFactory(logger, bindings, opts, req)).type.toBe<FastifyBaseLogger>()
    return server.childLoggerFactory(logger, bindings, opts, req)
  }
})).type.toBe<FastifyInstance>()

expect(
  server.setChildLoggerFactory(function (logger, bindings, opts, req) {
    expect(logger).type.toBe<FastifyBaseLogger>()
    expect(bindings).type.toBe<Bindings>()
    expect(opts).type.toBe<ChildLoggerOptions>()
    expect(req).type.toBe<RawRequestDefaultExpression>()
    expect(this).type.toBe<FastifyInstance>()
    return logger.child(bindings, opts)
  })
).type.toBe<FastifyInstance>()

expect(
  server.setErrorHandler<FastifyError>(function (error, request, reply) {
    expect(error).type.toBe<FastifyError>()
  })
).type.toBe<FastifyInstance>()

function childLoggerFactory (
  this: FastifyInstance,
  logger: FastifyBaseLogger,
  bindings: Bindings,
  opts: ChildLoggerOptions,
  req: RawRequestDefaultExpression
) {
  return logger.child(bindings, opts)
}
server.setChildLoggerFactory(childLoggerFactory)
server.setChildLoggerFactory(server.childLoggerFactory)

type InitialConfig = Readonly<{
  connectionTimeout?: number,
  keepAliveTimeout?: number,
  bodyLimit?: number,
  caseSensitive?: boolean,
  allowUnsafeRegex?: boolean,
  forceCloseConnections?: boolean,
  http2?: boolean,
  https?: boolean | Readonly<{ allowHTTP1: boolean }>,
  ignoreTrailingSlash?: boolean,
  ignoreDuplicateSlashes?: boolean,
  disableRequestLogging?: boolean | ((req: FastifyRequest) => boolean),
  maxParamLength?: number,
  onProtoPoisoning?: 'error' | 'remove' | 'ignore',
  onConstructorPoisoning?: 'error' | 'remove' | 'ignore',
  pluginTimeout?: number,
  requestIdHeader?: string | false,
  requestIdLogLabel?: string,
  http2SessionTimeout?: number,
  useSemicolonDelimiter?: boolean,
  routerOptions?: FastifyRouterOptions<RawServerDefault>
}>

expect(fastify().initialConfig).type.toBe<InitialConfig>()

expect<FastifyRouterOptions<RawServerDefault>>().type.toBeAssignableTo<
  FindMyWayConfig<FindMyWayVersion<RawServerDefault>>
>()

fastify({
  routerOptions: {
    defaultRoute: (req, res) => {
      expect(req).type.toBe<RawRequestDefaultExpression<RawServerDefault>>()
      expect(res).type.toBe<RawReplyDefaultExpression<RawServerDefault>>()
      expect(res).type.not.toBeAssignableTo<FastifyReply>()
      res.end('foo')
    },
    onBadUrl: (path, req, res) => {
      expect(path).type.toBe<string>()
      expect(req).type.toBe<RawRequestDefaultExpression<RawServerDefault>>()
      expect(res).type.toBe<RawReplyDefaultExpression<RawServerDefault>>()
      expect(res).type.not.toBeAssignableTo<FastifyReply>()
      res.end('foo')
    },
    onMaxParamLength (path, req, res) {
      expect(path).type.toBe<string>()
      expect(req).type.toBe<RawRequestDefaultExpression<RawServerDefault>>()
      expect(res).type.toBe<RawReplyDefaultExpression<RawServerDefault>>()
      expect(res).type.not.toBeAssignableTo<FastifyReply>()
      res.end('foo')
    }
  }
})

expect(server.defaultTextParser).type.toBe<FastifyBodyParser<string>>()

expect(server.getDefaultJsonParser('ignore', 'error')).type.toBe<FastifyBodyParser<string>>()

expect(server.printRoutes({ includeHooks: true, commonPrefix: false, includeMeta: true })).type.toBe<string>()

expect(server.printRoutes({ includeMeta: ['key1', Symbol('key2')] })).type.toBe<string>()

expect(server.printRoutes({ method: 'GET' })).type.toBe<string>()

expect(server.printRoutes()).type.toBe<string>()

server.decorate<(x: string) => void>('test', function (x: string): void {
  expect(this).type.toBe<FastifyInstance>()
})
server.decorate('test', function (x: string): void {
  expect(this).type.toBe<FastifyInstance>()
})
server.decorate<string>('test', {
  getter () {
    expect(this).type.toBe<FastifyInstance>()
    return 'foo'
  }
})
server.decorate<string>('test', {
  getter () {
    expect(this).type.toBe<FastifyInstance>()
    return 'foo'
  },
  setter (x) {
    expect(x).type.toBe<string>()
    expect(this).type.toBe<FastifyInstance>()
  }
})
server.decorate('test')
server.decorate('test', null, ['foo'])

server.decorateRequest<(x: string, y: number) => void>('test', function (x: string, y: number): void {
  expect(this).type.toBe<FastifyRequest>()
})
server.decorateRequest('test', function (x: string, y: number): void {
  expect(this).type.toBe<FastifyRequest>()
})
server.decorateRequest('test')
server.decorateRequest('test', null, ['foo'])

server.decorateReply<(x: string) => void>('test', function (x: string): void {
  expect(this).type.toBe<FastifyReply>()
})
server.decorateReply('test', function (x: string): void {
  expect(this).type.toBe<FastifyReply>()
})
server.decorateReply('test')
server.decorateReply('test', null, ['foo'])

// @ts-expect-error  Argument of type 'boolean' is not assignable to parameter of type 'GetterSetter...'.
server.decorate<string>('test', true)
// @ts-expect-error  Type 'string' is not assignable to type 'number'.
server.decorate<(myNumber: number) => number>('test', function (myNumber: number): string {
  return ''
})
server.decorate<string>('test', {
  // @ts-expect-error  Type 'boolean' is not assignable to type 'string'.
  getter () {
    return true
  }
})
// @ts-expect-error  Property 'getter' is missing in type
server.decorate<string>('test', {
  setter (x) { }
})

declare module '../../fastify' {
  interface FastifyInstance {
    typedTestProperty: boolean
    typedTestPropertyGetterSetter: string
    typedTestMethod(x: string): string
  }

  interface FastifyRequest {
    typedTestRequestProperty: boolean
    typedTestRequestPropertyGetterSetter: string
    typedTestRequestMethod(x: string): string
  }

  interface FastifyReply {
    typedTestReplyProperty: boolean
    typedTestReplyPropertyGetterSetter: string
    typedTestReplyMethod(x: string): string
  }
}

server.decorate('typedTestProperty', false)
server.decorate('typedTestProperty', {
  getter () {
    return false
  }
})
server.decorate('typedTestProperty', {
  getter (): boolean {
    return true
  },
  setter (x) {
    expect(x).type.toBe<boolean>()
    expect(this).type.toBe<FastifyInstance>()
  }
})
server.decorate('typedTestProperty')
server.decorate('typedTestProperty', null, ['foo'])
expect(server.decorate).type.not.toBeCallableWith('typedTestProperty', null)
expect(server.decorate).type.not.toBeCallableWith('typedTestProperty', 'foo')
expect(server.decorate).type.not.toBeCallableWith('typedTestProperty', {
  getter () {
    return 'foo'
  }
})
server.decorate('typedTestMethod', function (x) {
  expect(x).type.toBe<string>()
  expect(this).type.toBe<FastifyInstance>()
  return 'foo'
})
server.decorate('typedTestMethod', x => x)
// @ts-expect-error  Type 'string' is not assignable to type 'boolean'.
server.decorate('typedTestMethod', function (x: boolean) {
  return 'foo'
})
// @ts-expect-error  Type 'boolean' is not assignable to type 'string'.
server.decorate('typedTestMethod', function () {
  return true
})
// @ts-expect-error  Type 'Promise<string>' is not assignable to type 'string'.
server.decorate('typedTestMethod', async function () {
  return 'foo'
})

server.decorateRequest('typedTestRequestProperty', false)
server.decorateRequest('typedTestRequestProperty', {
  getter () {
    return false
  }
})
server.decorateRequest('typedTestRequestProperty', {
  getter (): boolean {
    return true
  },
  setter (x) {
    expect(x).type.toBe<boolean>()
    expect(this).type.toBe<FastifyRequest>()
  }
})
server.decorateRequest('typedTestRequestProperty')
server.decorateRequest('typedTestRequestProperty', null, ['foo'])
expect(server.decorateRequest).type.not.toBeCallableWith('typedTestRequestProperty', null)
expect(server.decorateRequest).type.not.toBeCallableWith('typedTestRequestProperty', 'foo')
expect(server.decorateRequest).type.not.toBeCallableWith('typedTestRequestProperty', {
  getter () {
    return 'foo'
  }
})
server.decorateRequest('typedTestRequestMethod', function (x) {
  expect(x).type.toBe<string>()
  expect(this).type.toBe<FastifyRequest>()
  return 'foo'
})
server.decorateRequest('typedTestRequestMethod', x => x)
// @ts-expect-error  Type 'string' is not assignable to type 'boolean'.
server.decorateRequest('typedTestRequestMethod', function (x: boolean) {
  return 'foo'
})
// @ts-expect-error  Type 'boolean' is not assignable to type 'string'.
server.decorateRequest('typedTestRequestMethod', function () {
  return true
})
// @ts-expect-error  Type 'Promise<string>' is not assignable to type 'string'.
server.decorateRequest('typedTestRequestMethod', async function () {
  return 'foo'
})

server.decorateReply('typedTestReplyProperty', false)
server.decorateReply('typedTestReplyProperty', {
  getter () {
    return false
  }
})
server.decorateReply('typedTestReplyProperty', {
  getter (): boolean {
    return true
  },
  setter (x) {
    expect(x).type.toBe<boolean>()
    expect(this).type.toBe<FastifyReply>()
  }
})
server.decorateReply('typedTestReplyProperty')
server.decorateReply('typedTestReplyProperty', null, ['foo'])
expect(server.decorateReply).type.not.toBeCallableWith('typedTestReplyProperty', null)
expect(server.decorateReply).type.not.toBeCallableWith('typedTestReplyProperty', 'foo')
expect(server.decorateReply).type.not.toBeCallableWith('typedTestReplyProperty', {
  getter () {
    return 'foo'
  }
})
server.decorateReply('typedTestReplyMethod', function (x) {
  expect(x).type.toBe<string>()
  expect(this).type.toBe<FastifyReply>()
  return 'foo'
})
server.decorateReply('typedTestReplyMethod', x => x)
// @ts-expect-error  Type 'string' is not assignable to type 'boolean'
server.decorateReply('typedTestReplyMethod', function (x: boolean) {
  return 'foo'
})
// @ts-expect-error  Type 'boolean' is not assignable to type 'string'
server.decorateReply('typedTestReplyMethod', function (x) {
  return true
})
// @ts-expect-error  Type 'Promise<string>' is not assignable to type 'string'.
server.decorateReply('typedTestReplyMethod', async function () {
  return 'foo'
})

const foo = server.getDecorator<string>('foo')
expect(foo).type.toBe<string>()

const versionConstraintStrategy: ConstraintStrategy<FindMyWayVersion<RawServerDefault>> = {
  name: 'version',
  storage: () => ({
    get: () => () => { },
    set: () => { },
    del: () => { },
    empty: () => { }
  }),
  validate () { },
  deriveConstraint: () => 'foo'
}
expect(server.addConstraintStrategy(versionConstraintStrategy)).type.toBe<void>()
expect(server.hasConstraintStrategy(versionConstraintStrategy.name)).type.toBe<boolean>()

expect(server.validatorCompiler).type.toBe<FastifySchemaCompiler<any> | undefined>()
expect(server.serializerCompiler).type.toBe<FastifySerializerCompiler<any> | undefined>()
