import { FastifyError } from '@fastify/error'
import * as http from 'http'
import { expectAssignable, expectError, expectType } from 'tsd'
import fastify, { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from '../../fastify'
import { RequestPayload } from '../../types/hooks'
import { FindMyWayFindResult } from '../../types/instance'
import { HTTPMethods, RawServerDefault } from '../../types/utils'

/*
 * Testing Fastify HTTP Routes and Route Shorthands.
 * Verifies Request and Reply types as well.
 * For the route shorthand tests the argument orders are:
 * - `(path, handler)`
 * - `(path, options, handler)`
 * - `(path, options)`
 */

declare module '../../fastify' {
  interface FastifyContextConfig {
    foo: string;
    bar: number;
    includeMessage?: boolean;
  }

  interface FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider, ContextConfig, Logger, RequestType> {
    message: ContextConfig extends { includeMessage: true }
      ? string
      : null;
  }
}

const routeHandler: RouteHandlerMethod = function (request, reply) {
  expectType<FastifyInstance>(this)
  expectType<FastifyRequest>(request)
  expectType<FastifyReply>(reply)
}

const routeHandlerWithReturnValue: RouteHandlerMethod = function (request, reply) {
  expectType<FastifyInstance>(this)
  expectType<FastifyRequest>(request)
  expectType<FastifyReply>(reply)

  return reply.send()
}

fastify().get(
  '/',
  { config: { foo: 'bar', bar: 100, includeMessage: true } },
  (req) => {
    expectType<string>(req.message)
  }
)

fastify().get(
  '/',
  { config: { foo: 'bar', bar: 100, includeMessage: false } },
  (req) => {
    expectType<null>(req.message)
  }
)

type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options';

['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE', 'OPTIONS'].forEach(method => {
  // route method
  expectType<FastifyInstance>(fastify().route({
    method: method as HTTPMethods,
    url: '/',
    handler: routeHandler
  }))

  const lowerCaseMethod: LowerCaseHTTPMethods = method.toLowerCase() as LowerCaseHTTPMethods

  // method as method
  expectType<FastifyInstance>(fastify()[lowerCaseMethod]('/', routeHandler))
  expectType<FastifyInstance>(fastify()[lowerCaseMethod]('/', {}, routeHandler))
  expectType<FastifyInstance>(fastify()[lowerCaseMethod]('/', { handler: routeHandler }))

  expectType<FastifyInstance>(fastify()[lowerCaseMethod]('/', {
    handler: routeHandler,
    errorHandler: (error, request, reply) => {
      expectType<FastifyError>(error)
      reply.send('error')
    },
    childLoggerFactory: function (logger, bindings, opts) {
      return logger.child(bindings, opts)
    }
  }))

  interface BodyInterface { prop: string }
  interface QuerystringInterface { prop: number }
  interface ParamsInterface { prop: boolean }
  interface HeadersInterface { prop: string }
  interface RouteSpecificContextConfigType {
    extra: boolean
  }
  interface RouteGeneric {
    Body: BodyInterface;
    Querystring: QuerystringInterface;
    Params: ParamsInterface;
    Headers: HeadersInterface;
  }

  fastify()[lowerCaseMethod]<RouteGeneric, RouteSpecificContextConfigType>('/', { config: { foo: 'bar', bar: 100, extra: true } }, (req, res) => {
    expectType<BodyInterface>(req.body)
    expectType<QuerystringInterface>(req.query)
    expectType<ParamsInterface>(req.params)
    expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
    expectType<string>(req.context.config.foo)
    expectType<number>(req.context.config.bar)
    expectType<boolean>(req.context.config.extra)
    expectType<string>(req.context.config.url)
    expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
    expectType<string>(res.context.config.foo)
    expectType<number>(res.context.config.bar)
    expectType<boolean>(res.context.config.extra)
    expectType<string>(req.routeConfig.url)
    expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
  })

  fastify().route<RouteGeneric>({
    url: '/',
    method: method as HTTPMethods,
    config: { foo: 'bar', bar: 100 },
    prefixTrailingSlash: 'slash',
    onRequest: (req, res, done) => { // these handlers are tested in `hooks.test-d.ts`
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preParsing: (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
      expectType<RequestPayload>(payload)
      expectAssignable<(err?: FastifyError | null, res?: RequestPayload) => void>(done)
      expectAssignable<(err?: NodeJS.ErrnoException) => void>(done)
    },
    preValidation: (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preHandler: (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    onResponse: (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
      expectType<number>(res.statusCode)
    },
    onError: (req, res, error, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preSerialization: (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    onSend: (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    handler: (req, res) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    }
  })

  fastify().route<RouteGeneric>({
    url: '/',
    method: method as HTTPMethods,
    config: { foo: 'bar', bar: 100 },
    prefixTrailingSlash: 'slash',
    onRequest: async (req, res, done) => { // these handlers are tested in `hooks.test-d.ts`
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preParsing: async (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
      expectType<RequestPayload>(payload)
      expectAssignable<(err?: FastifyError | null, res?: RequestPayload) => void>(done)
      expectAssignable<(err?: NodeJS.ErrnoException) => void>(done)
    },
    preValidation: async (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preHandler: async (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    onResponse: async (req, res, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
      expectType<number>(res.statusCode)
    },
    onError: async (req, res, error, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    preSerialization: async (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    onSend: async (req, res, payload, done) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    },
    handler: (req, res) => {
      expectType<BodyInterface>(req.body)
      expectType<QuerystringInterface>(req.query)
      expectType<ParamsInterface>(req.params)
      expectType<http.IncomingHttpHeaders & HeadersInterface>(req.headers)
      expectType<string>(req.context.config.foo)
      expectType<number>(req.context.config.bar)
      expectType<string>(req.context.config.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method)
      expectType<string>(res.context.config.foo)
      expectType<number>(res.context.config.bar)
      expectType<string>(req.routeConfig.url)
      expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method)
    }
  })
})

expectError(fastify().route({
  url: '/',
  method: 'CONNECT', // not a valid method
  handler: routeHandler
}))

expectType<FastifyInstance>(fastify().route({
  url: '/',
  method: ['GET', 'POST'],
  handler: routeHandler
}))

expectError(fastify().route({
  url: '/',
  method: ['GET', 'POST', 'OPTION'], // OPTION is a typo for OPTIONS
  handler: routeHandler
}))

expectError(fastify().route({
  url: '/',
  method: 'GET',
  handler: routeHandler,
  schemaErrorFormatter: 500 // Not a valid formatter
}))

expectType<FastifyInstance>(fastify().route({
  url: '/',
  method: 'GET',
  handler: routeHandler,
  schemaErrorFormatter: (errors, dataVar) => new Error('')
}))

expectError(fastify().route({
  prefixTrailingSlash: true // Not a valid value
}))

expectType<FastifyInstance>(fastify().route({
  url: '/',
  method: 'GET',
  handler: routeHandlerWithReturnValue
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET'
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET',
  constraints: { version: '1.2.0' }
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET',
  constraints: { host: 'auth.fastify.dev' }
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET',
  constraints: { host: /.*\.fastify\.dev$/ }
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET',
  constraints: { host: /.*\.fastify\.dev$/, version: '1.2.3' }
}))

expectType<boolean>(fastify().hasRoute({
  url: '/',
  method: 'GET',
  constraints: {
    // constraints value should accept any value
    number: 12,
    date: new Date(),
    boolean: true,
    function: () => {},
    object: { foo: 'bar' }
  }
}))

expectType<Omit<FindMyWayFindResult<RawServerDefault>, 'store'>>(
  fastify().findRoute({
    url: '/',
    method: 'get'
  })
)

// we should not expose store
expectError(fastify().findRoute({
  url: '/',
  method: 'get'
}).store)

expectType<FastifyInstance>(fastify().route({
  url: '/',
  method: 'get',
  handler: routeHandlerWithReturnValue
}))

expectType<FastifyInstance>(fastify().route({
  url: '/',
  method: ['put', 'patch'],
  handler: routeHandlerWithReturnValue
}))
