'use strict'

const t = require('tap')
const test = t.test
const Fastify = require('..')
const fp = require('fastify-plugin')
const sget = require('simple-get').concat
const symbols = require('../lib/symbols.js')

test('server methods should exist', t => {
  t.plan(2)
  const fastify = Fastify()
  t.ok(fastify.decorate)
  t.ok(fastify.hasDecorator)
})

test('should check if the given decoration already exist when null', t => {
  t.plan(1)
  const fastify = Fastify()
  fastify.decorate('null', null)
  fastify.ready(() => {
    t.ok(fastify.hasDecorator('null'))
  })
})

test('server methods should be encapsulated via .register', t => {
  t.plan(2)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorate('test', () => {})
    t.ok(instance.test)
    done()
  })

  fastify.ready(() => {
    t.notOk(fastify.test)
  })
})

test('hasServerMethod should check if the given method already exist', t => {
  t.plan(2)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorate('test', () => {})
    t.ok(instance.hasDecorator('test'))
    done()
  })

  fastify.ready(() => {
    t.notOk(fastify.hasDecorator('test'))
  })
})

test('decorate should throw if a declared dependency is not present', t => {
  t.plan(3)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    try {
      instance.decorate('test', () => {}, ['dependency'])
      t.fail()
    } catch (e) {
      t.same(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY')
      t.same(e.message, 'The decorator is missing dependency \'dependency\'.')
    }
    done()
  })

  fastify.ready(() => t.pass())
})

test('decorate should throw if declared dependency is not array', t => {
  t.plan(3)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    try {
      instance.decorate('test', () => {}, {})
      t.fail()
    } catch (e) {
      t.same(e.code, 'FST_ERR_DEC_DEPENDENCY_INVALID_TYPE')
      t.same(e.message, 'The dependencies of decorator \'test\' must be of type Array.')
    }
    done()
  })

  fastify.ready(() => t.pass())
})

// issue #777
test('should pass error for missing request decorator', t => {
  t.plan(2)
  const fastify = Fastify()

  const plugin = fp(function (instance, opts, done) {
    done()
  }, {
    decorators: {
      request: ['foo']
    }
  })
  fastify
    .register(plugin)
    .ready((err) => {
      t.type(err, Error)
      t.match(err, /The decorator 'foo'/)
    })
})

test('decorateReply inside register', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorateReply('test', 'test')

    instance.get('/yes', (req, reply) => {
      t.ok(reply.test, 'test exists')
      reply.send({ hello: 'world' })
    })

    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(reply.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateReply as plugin (inside .after)', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.register(fp((i, o, n) => {
      instance.decorateReply('test', 'test')
      n()
    })).after(() => {
      instance.get('/yes', (req, reply) => {
        t.ok(reply.test)
        reply.send({ hello: 'world' })
      })
    })
    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(reply.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateReply as plugin (outside .after)', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.register(fp((i, o, n) => {
      instance.decorateReply('test', 'test')
      n()
    }))

    instance.get('/yes', (req, reply) => {
      t.ok(reply.test)
      reply.send({ hello: 'world' })
    })
    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(reply.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest inside register', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorateRequest('test', 'test')

    instance.get('/yes', (req, reply) => {
      t.ok(req.test, 'test exists')
      reply.send({ hello: 'world' })
    })

    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(req.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest as plugin (inside .after)', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.register(fp((i, o, n) => {
      instance.decorateRequest('test', 'test')
      n()
    })).after(() => {
      instance.get('/yes', (req, reply) => {
        t.ok(req.test)
        reply.send({ hello: 'world' })
      })
    })
    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(req.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest as plugin (outside .after)', t => {
  t.plan(11)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.register(fp((i, o, n) => {
      instance.decorateRequest('test', 'test')
      n()
    }))

    instance.get('/yes', (req, reply) => {
      t.ok(req.test)
      reply.send({ hello: 'world' })
    })
    done()
  })

  fastify.get('/no', (req, reply) => {
    t.notOk(req.test)
    reply.send({ hello: 'world' })
  })

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/no'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorators should be instance separated', t => {
  t.plan(1)

  const fastify1 = Fastify()
  const fastify2 = Fastify()

  fastify1.decorate('test', 'foo')
  fastify2.decorate('test', 'foo')

  fastify1.decorateRequest('test', 'foo')
  fastify2.decorateRequest('test', 'foo')

  fastify1.decorateReply('test', 'foo')
  fastify2.decorateReply('test', 'foo')

  t.pass()
})

test('hasRequestDecorator', t => {
  const requestDecoratorName = 'my-decorator-name'

  t.test('is a function', t => {
    t.plan(1)
    const fastify = Fastify()
    t.ok(fastify.hasRequestDecorator)
  })

  t.test('should check if the given request decoration already exist', t => {
    t.plan(2)
    const fastify = Fastify()

    t.notOk(fastify.hasRequestDecorator(requestDecoratorName))
    fastify.decorateRequest(requestDecoratorName, 42)
    t.ok(fastify.hasRequestDecorator(requestDecoratorName))
  })

  t.test('should check if the given request decoration already exist when null', t => {
    t.plan(2)
    const fastify = Fastify()

    t.notOk(fastify.hasRequestDecorator(requestDecoratorName))
    fastify.decorateRequest(requestDecoratorName, null)
    t.ok(fastify.hasRequestDecorator(requestDecoratorName))
  })

  t.test('should be plugin encapsulable', t => {
    t.plan(4)
    const fastify = Fastify()

    t.notOk(fastify.hasRequestDecorator(requestDecoratorName))

    fastify.register(function (fastify2, opts, done) {
      fastify2.decorateRequest(requestDecoratorName, 42)
      t.ok(fastify2.hasRequestDecorator(requestDecoratorName))
      done()
    })

    t.notOk(fastify.hasRequestDecorator(requestDecoratorName))

    fastify.ready(function () {
      t.notOk(fastify.hasRequestDecorator(requestDecoratorName))
    })
  })

  t.test('should be inherited', t => {
    t.plan(2)
    const fastify = Fastify()

    fastify.decorateRequest(requestDecoratorName, 42)

    fastify.register(function (fastify2, opts, done) {
      t.ok(fastify2.hasRequestDecorator(requestDecoratorName))
      done()
    })

    fastify.ready(function () {
      t.ok(fastify.hasRequestDecorator(requestDecoratorName))
    })
  })

  t.end()
})

test('hasReplyDecorator', t => {
  const replyDecoratorName = 'my-decorator-name'

  t.test('is a function', t => {
    t.plan(1)
    const fastify = Fastify()
    t.ok(fastify.hasReplyDecorator)
  })

  t.test('should check if the given reply decoration already exist', t => {
    t.plan(2)
    const fastify = Fastify()

    t.notOk(fastify.hasReplyDecorator(replyDecoratorName))
    fastify.decorateReply(replyDecoratorName, 42)
    t.ok(fastify.hasReplyDecorator(replyDecoratorName))
  })

  t.test('should check if the given reply decoration already exist when null', t => {
    t.plan(2)
    const fastify = Fastify()

    t.notOk(fastify.hasReplyDecorator(replyDecoratorName))
    fastify.decorateReply(replyDecoratorName, null)
    t.ok(fastify.hasReplyDecorator(replyDecoratorName))
  })

  t.test('should be plugin encapsulable', t => {
    t.plan(4)
    const fastify = Fastify()

    t.notOk(fastify.hasReplyDecorator(replyDecoratorName))

    fastify.register(function (fastify2, opts, done) {
      fastify2.decorateReply(replyDecoratorName, 42)
      t.ok(fastify2.hasReplyDecorator(replyDecoratorName))
      done()
    })

    t.notOk(fastify.hasReplyDecorator(replyDecoratorName))

    fastify.ready(function () {
      t.notOk(fastify.hasReplyDecorator(replyDecoratorName))
    })
  })

  t.test('should be inherited', t => {
    t.plan(2)
    const fastify = Fastify()

    fastify.decorateReply(replyDecoratorName, 42)

    fastify.register(function (fastify2, opts, done) {
      t.ok(fastify2.hasReplyDecorator(replyDecoratorName))
      done()
    })

    fastify.ready(function () {
      t.ok(fastify.hasReplyDecorator(replyDecoratorName))
    })
  })

  t.end()
})

test('should register properties via getter/setter objects', t => {
  t.plan(3)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorate('test', {
      getter () {
        return 'a getter'
      }
    })
    t.ok(instance.test)
    t.ok(instance.test, 'a getter')
    done()
  })

  fastify.ready(() => {
    t.notOk(fastify.test)
  })
})

test('decorateRequest should work with getter/setter', t => {
  t.plan(5)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorateRequest('test', {
      getter () {
        return 'a getter'
      }
    })

    instance.get('/req-decorated-get-set', (req, res) => {
      res.send({ test: req.test })
    })

    done()
  })

  fastify.get('/not-decorated', (req, res) => {
    t.notOk(req.test)
    res.send()
  })

  fastify.ready(() => {
    fastify.inject({ url: '/req-decorated-get-set' }, (err, res) => {
      t.error(err)
      t.same(JSON.parse(res.payload), { test: 'a getter' })
    })

    fastify.inject({ url: '/not-decorated' }, (err, res) => {
      t.error(err)
      t.pass()
    })
  })
})

test('decorateReply should work with getter/setter', t => {
  t.plan(5)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorateReply('test', {
      getter () {
        return 'a getter'
      }
    })

    instance.get('/res-decorated-get-set', (req, res) => {
      res.send({ test: res.test })
    })

    done()
  })

  fastify.get('/not-decorated', (req, res) => {
    t.notOk(res.test)
    res.send()
  })

  fastify.ready(() => {
    fastify.inject({ url: '/res-decorated-get-set' }, (err, res) => {
      t.error(err)
      t.same(JSON.parse(res.payload), { test: 'a getter' })
    })

    fastify.inject({ url: '/not-decorated' }, (err, res) => {
      t.error(err)
      t.pass()
    })
  })
})

test('should register empty values', t => {
  t.plan(2)
  const fastify = Fastify()

  fastify.register((instance, opts, done) => {
    instance.decorate('test', null)
    t.ok(Object.hasOwn(instance, 'test'))
    done()
  })

  fastify.ready(() => {
    t.notOk(fastify.test)
  })
})

test('nested plugins can override things', t => {
  t.plan(6)
  const fastify = Fastify()

  const rootFunc = () => {}
  fastify.decorate('test', rootFunc)
  fastify.decorateRequest('test', rootFunc)
  fastify.decorateReply('test', rootFunc)

  fastify.register((instance, opts, done) => {
    const func = () => {}
    instance.decorate('test', func)
    instance.decorateRequest('test', func)
    instance.decorateReply('test', func)

    t.equal(instance.test, func)
    t.equal(instance[symbols.kRequest].prototype.test, func)
    t.equal(instance[symbols.kReply].prototype.test, func)
    done()
  })

  fastify.ready(() => {
    t.equal(fastify.test, rootFunc)
    t.equal(fastify[symbols.kRequest].prototype.test, rootFunc)
    t.equal(fastify[symbols.kReply].prototype.test, rootFunc)
  })
})

test('a decorator should addSchema to all the encapsulated tree', t => {
  t.plan(1)
  const fastify = Fastify()

  const decorator = function (instance, opts, done) {
    instance.decorate('decoratorAddSchema', function (whereAddTheSchema) {
      instance.addSchema({
        $id: 'schema',
        type: 'string'
      })
    })
    done()
  }

  fastify.register(fp(decorator))

  fastify.register(function (instance, opts, done) {
    instance.register((subInstance, opts, done) => {
      subInstance.decoratorAddSchema()
      done()
    })
    done()
  })

  fastify.ready(t.error)
})

test('after can access to a decorated instance and previous plugin decoration', t => {
  t.plan(11)
  const TEST_VALUE = {}
  const OTHER_TEST_VALUE = {}
  const NEW_TEST_VALUE = {}

  const fastify = Fastify()

  fastify.register(fp(function (instance, options, done) {
    instance.decorate('test', TEST_VALUE)

    done()
  })).after(function (err, instance, done) {
    t.error(err)
    t.equal(instance.test, TEST_VALUE)

    instance.decorate('test2', OTHER_TEST_VALUE)
    done()
  })

  fastify.register(fp(function (instance, options, done) {
    t.equal(instance.test, TEST_VALUE)
    t.equal(instance.test2, OTHER_TEST_VALUE)

    instance.decorate('test3', NEW_TEST_VALUE)

    done()
  })).after(function (err, instance, done) {
    t.error(err)
    t.equal(instance.test, TEST_VALUE)
    t.equal(instance.test2, OTHER_TEST_VALUE)
    t.equal(instance.test3, NEW_TEST_VALUE)

    done()
  })

  fastify.get('/', function (req, res) {
    t.equal(this.test, TEST_VALUE)
    t.equal(this.test2, OTHER_TEST_VALUE)
    res.send({})
  })

  fastify.inject('/')
    .then(response => {
      t.equal(response.statusCode, 200)
    })
})

test('decorate* should throw if called after ready', async t => {
  t.plan(6)
  const fastify = Fastify()

  fastify.get('/', (request, reply) => {
    reply.send({
      hello: 'world'
    })
  })

  await fastify.listen({ port: 0 })
  try {
    fastify.decorate('test', true)
    t.fail('should not decorate')
  } catch (err) {
    t.same(err.code, 'FST_ERR_DEC_AFTER_START')
    t.same(err.message, "The decorator 'test' has been added after start!")
  }
  try {
    fastify.decorateRequest('test', true)
    t.fail('should not decorate')
  } catch (e) {
    t.same(e.code, 'FST_ERR_DEC_AFTER_START')
    t.same(e.message, "The decorator 'test' has been added after start!")
  }
  try {
    fastify.decorateReply('test', true)
    t.fail('should not decorate')
  } catch (e) {
    t.same(e.code, 'FST_ERR_DEC_AFTER_START')
    t.same(e.message, "The decorator 'test' has been added after start!")
  }
  await fastify.close()
})

test('decorate* should emit error if an array is passed', t => {
  t.plan(2)

  const fastify = Fastify()
  try {
    fastify.decorateRequest('test_array', [])
    t.fail('should not decorate')
  } catch (err) {
    t.same(err.code, 'FST_ERR_DEC_REFERENCE_TYPE')
    t.same(err.message, "The decorator 'test_array' of type 'object' is a reference type. Use the { getter, setter } interface instead.")
  }
})

test('server.decorate should not emit error if reference type is passed', async t => {
  t.plan(1)

  const fastify = Fastify()
  fastify.decorate('test_array', [])
  fastify.decorate('test_object', {})
  await fastify.ready()
  t.pass('Done')
})

test('decorate* should emit warning if object type is passed', t => {
  t.plan(2)

  const fastify = Fastify()
  try {
    fastify.decorateRequest('test_object', { foo: 'bar' })
    t.fail('should not decorate')
  } catch (err) {
    t.same(err.code, 'FST_ERR_DEC_REFERENCE_TYPE')
    t.same(err.message, "The decorator 'test_object' of type 'object' is a reference type. Use the { getter, setter } interface instead.")
  }
})

test('decorate* should not emit warning if object with getter/setter is passed', t => {
  const fastify = Fastify()

  fastify.decorateRequest('test_getter_setter', {
    setter (val) {
      this._ = val
    },
    getter () {
      return 'a getter'
    }
  })
  t.end('Done')
})

test('decorateRequest with getter/setter can handle encapsulation', async t => {
  t.plan(24)

  const fastify = Fastify({ logger: true })

  fastify.decorateRequest('test_getter_setter_holder')
  fastify.decorateRequest('test_getter_setter', {
    getter () {
      this.test_getter_setter_holder ??= {}
      return this.test_getter_setter_holder
    }
  })

  fastify.get('/', async function (req, reply) {
    t.same(req.test_getter_setter, {}, 'a getter')
    req.test_getter_setter.a = req.id
    t.same(req.test_getter_setter, { a: req.id })
  })

  fastify.addHook('onResponse', async function hook (req, reply) {
    t.same(req.test_getter_setter, { a: req.id })
  })

  await Promise.all([
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200))
  ])
})

test('decorateRequest with getter/setter can handle encapsulation with arrays', async t => {
  t.plan(24)

  const fastify = Fastify({ logger: true })

  fastify.decorateRequest('array_holder')
  fastify.decorateRequest('my_array', {
    getter () {
      this.array_holder ??= []
      return this.array_holder
    }
  })

  fastify.get('/', async function (req, reply) {
    t.same(req.my_array, [])
    req.my_array.push(req.id)
    t.same(req.my_array, [req.id])
  })

  fastify.addHook('onResponse', async function hook (req, reply) {
    t.same(req.my_array, [req.id])
  })

  await Promise.all([
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200)),
    fastify.inject('/').then(res => t.same(res.statusCode, 200))
  ])
})

test('decorate* should not emit error if string,bool,numbers are passed', t => {
  const fastify = Fastify()

  fastify.decorateRequest('test_str', 'foo')
  fastify.decorateRequest('test_bool', true)
  fastify.decorateRequest('test_number', 42)
  fastify.decorateRequest('test_null', null)
  fastify.decorateRequest('test_undefined', undefined)
  fastify.decorateReply('test_str', 'foo')
  fastify.decorateReply('test_bool', true)
  fastify.decorateReply('test_number', 42)
  fastify.decorateReply('test_null', null)
  fastify.decorateReply('test_undefined', undefined)
  t.end('Done')
})

test('Request/reply decorators should be able to access the server instance', async t => {
  t.plan(6)

  const server = require('..')({ logger: false })
  server.decorateRequest('assert', rootAssert)
  server.decorateReply('assert', rootAssert)

  server.get('/root-assert', async (req, res) => {
    req.assert()
    res.assert()
    return 'done'
  })

  server.register(async instance => {
    instance.decorateRequest('assert', nestedAssert)
    instance.decorateReply('assert', nestedAssert)
    instance.decorate('foo', 'bar')

    instance.get('/nested-assert', async (req, res) => {
      req.assert()
      res.assert()
      return 'done'
    })
  })

  await server.inject({ method: 'GET', url: '/root-assert' })
  await server.inject({ method: 'GET', url: '/nested-assert' })

  // ----
  function rootAssert () {
    t.equal(this.server, server)
  }

  function nestedAssert () {
    t.not(this.server, server)
    t.equal(this.server.foo, 'bar')
  }
})

test('plugin required decorators', async t => {
  const plugin1 = fp(
    async (instance) => {
      instance.decorateRequest('someThing', null)

      instance.addHook('onRequest', async (request, reply) => {
        request.someThing = 'hello'
      })
    },
    {
      name: 'custom-plugin-one'
    }
  )

  const plugin2 = fp(
    async () => {
      // nothing
    },
    {
      name: 'custom-plugin-two',
      dependencies: ['custom-plugin-one'],
      decorators: {
        request: ['someThing']
      }
    }
  )

  const app = Fastify()
  app.register(plugin1)
  app.register(plugin2)
  await app.ready()
})

test('decorateRequest/decorateReply empty string', t => {
  t.plan(7)
  const fastify = Fastify()

  fastify.decorateRequest('test', '')
  fastify.decorateReply('test2', '')
  fastify.get('/yes', (req, reply) => {
    t.equal(req.test, '')
    t.equal(reply.test2, '')
    reply.send({ hello: 'world' })
  })
  t.teardown(fastify.close.bind(fastify))

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest/decorateReply is undefined', t => {
  t.plan(7)
  const fastify = Fastify()

  fastify.decorateRequest('test', undefined)
  fastify.decorateReply('test2', undefined)
  fastify.get('/yes', (req, reply) => {
    t.equal(req.test, undefined)
    t.equal(reply.test2, undefined)
    reply.send({ hello: 'world' })
  })
  t.teardown(fastify.close.bind(fastify))

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest/decorateReply is not set to a value', t => {
  t.plan(7)
  const fastify = Fastify()

  fastify.decorateRequest('test')
  fastify.decorateReply('test2')
  fastify.get('/yes', (req, reply) => {
    t.equal(req.test, undefined)
    t.equal(reply.test2, undefined)
    reply.send({ hello: 'world' })
  })
  t.teardown(fastify.close.bind(fastify))

  fastify.listen({ port: 0 }, err => {
    t.error(err)
    t.teardown(() => { fastify.close() })

    sget({
      method: 'GET',
      url: 'http://localhost:' + fastify.server.address().port + '/yes'
    }, (err, response, body) => {
      t.error(err)
      t.equal(response.statusCode, 200)
      t.equal(response.headers['content-length'], '' + body.length)
      t.same(JSON.parse(body), { hello: 'world' })
    })
  })
})

test('decorateRequest with dependencies', (t) => {
  t.plan(2)
  const app = Fastify()

  const decorator1 = 'bar'
  const decorator2 = 'foo'

  app.decorate('decorator1', decorator1)
  app.decorateRequest('decorator1', decorator1)

  if (
    app.hasDecorator('decorator1') &&
    app.hasRequestDecorator('decorator1')
  ) {
    t.doesNotThrow(() => app.decorateRequest('decorator2', decorator2, ['decorator1']))
    t.ok(app.hasRequestDecorator('decorator2'))
  }
})

test('decorateRequest with dependencies (functions)', (t) => {
  t.plan(2)
  const app = Fastify()

  const decorator1 = () => 'bar'
  const decorator2 = () => 'foo'

  app.decorate('decorator1', decorator1)
  app.decorateRequest('decorator1', decorator1)

  if (
    app.hasDecorator('decorator1') &&
    app.hasRequestDecorator('decorator1')
  ) {
    t.doesNotThrow(() => app.decorateRequest('decorator2', decorator2, ['decorator1']))
    t.ok(app.hasRequestDecorator('decorator2'))
  }
})

test('chain of decorators on Request', async (t) => {
  const fastify = Fastify()
  fastify.register(fp(async function (fastify) {
    fastify.decorateRequest('foo', 'toto')
    fastify.decorateRequest('bar', () => 'tata')
  }, {
    name: 'first'
  }))

  fastify.get('/foo', async function (request, reply) {
    return request.foo
  })
  fastify.get('/bar', function (request, reply) {
    return request.bar()
  })
  fastify.register(async function second (fastify) {
    fastify.get('/foo', async function (request, reply) {
      return request.foo
    })
    fastify.get('/bar', async function (request, reply) {
      return request.bar()
    })
    fastify.register(async function fourth (fastify) {
      fastify.get('/plugin3/foo', async function (request, reply) {
        return request.foo
      })
      fastify.get('/plugin3/bar', function (request, reply) {
        return request.bar()
      })
    })
    fastify.register(fp(async function (fastify) {
      fastify.decorateRequest('fooB', 'toto')
      fastify.decorateRequest('barB', () => 'tata')
    }, {
      name: 'third'
    }))
  },
  { prefix: '/plugin2', name: 'plugin2' }
  )

  await fastify.ready()

  {
    const response = await fastify.inject('/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/bar')
    t.equal(response.body, 'tata')
  }

  {
    const response = await fastify.inject('/plugin2/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/plugin2/bar')
    t.equal(response.body, 'tata')
  }

  {
    const response = await fastify.inject('/plugin2/plugin3/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/plugin2/plugin3/bar')
    t.equal(response.body, 'tata')
  }
})

test('chain of decorators on Reply', async (t) => {
  const fastify = Fastify()
  fastify.register(fp(async function (fastify) {
    fastify.decorateReply('foo', 'toto')
    fastify.decorateReply('bar', () => 'tata')
  }, {
    name: 'first'
  }))

  fastify.get('/foo', async function (request, reply) {
    return reply.foo
  })
  fastify.get('/bar', function (request, reply) {
    return reply.bar()
  })
  fastify.register(async function second (fastify) {
    fastify.get('/foo', async function (request, reply) {
      return reply.foo
    })
    fastify.get('/bar', async function (request, reply) {
      return reply.bar()
    })
    fastify.register(async function fourth (fastify) {
      fastify.get('/plugin3/foo', async function (request, reply) {
        return reply.foo
      })
      fastify.get('/plugin3/bar', function (request, reply) {
        return reply.bar()
      })
    })
    fastify.register(fp(async function (fastify) {
      fastify.decorateReply('fooB', 'toto')
      fastify.decorateReply('barB', () => 'tata')
    }, {
      name: 'third'
    }))
  },
  { prefix: '/plugin2', name: 'plugin2' }
  )

  await fastify.ready()

  {
    const response = await fastify.inject('/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/bar')
    t.equal(response.body, 'tata')
  }

  {
    const response = await fastify.inject('/plugin2/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/plugin2/bar')
    t.equal(response.body, 'tata')
  }

  {
    const response = await fastify.inject('/plugin2/plugin3/foo')
    t.equal(response.body, 'toto')
  }

  {
    const response = await fastify.inject('/plugin2/plugin3/bar')
    t.equal(response.body, 'tata')
  }
})

test('getDecorator should return the decorator', t => {
  t.plan(12)
  const fastify = Fastify()

  fastify.decorate('root', 'from_root')
  fastify.decorateRequest('root', 'from_root_request')
  fastify.decorateReply('root', 'from_root_reply')

  t.equal(fastify.getDecorator('root'), 'from_root')
  fastify.get('/', async (req, res) => {
    t.equal(req.getDecorator('root'), 'from_root_request')
    t.equal(res.getDecorator('root'), 'from_root_reply')

    res.send()
  })

  fastify.register((child) => {
    child.decorate('child', 'from_child')

    t.equal(child.getDecorator('child'), 'from_child')
    t.equal(child.getDecorator('root'), 'from_root')

    child.get('/child', async (req, res) => {
      t.equal(req.getDecorator('root'), 'from_root_request')
      t.equal(res.getDecorator('root'), 'from_root_reply')

      res.send()
    })
  })

  fastify.ready((err) => {
    t.error(err)
    fastify.inject({ url: '/' }, (err, res) => {
      t.error(err)
      t.pass()
    })

    fastify.inject({ url: '/child' }, (err, res) => {
      t.error(err)
      t.pass()
    })
  })
})

test('getDecorator should return function decorators with expected binded context', t => {
  t.plan(12)
  const fastify = Fastify()

  fastify.decorate('a', function () {
    return this
  })
  fastify.decorateRequest('b', function () {
    return this
  })
  fastify.decorateReply('c', function () {
    return this
  })

  fastify.register((child) => {
    child.decorate('a', function () {
      return this
    })

    t.same(child.getDecorator('a')(), child)
    child.get('/child', async (req, res) => {
      t.same(req.getDecorator('b')(), req)
      t.same(res.getDecorator('c')(), res)

      res.send()
    })
  })

  t.same(fastify.getDecorator('a')(), fastify)
  fastify.get('/', async (req, res) => {
    t.same(req.getDecorator('b')(), req)
    t.same(res.getDecorator('c')(), res)

    res.send()
  })

  fastify.ready((err) => {
    t.error(err)
    fastify.inject({ url: '/' }, (err, res) => {
      t.error(err)
      t.pass()
    })

    fastify.inject({ url: '/child' }, (err, res) => {
      t.error(err)
      t.pass()
    })

    t.pass()
  })
})

test('getDecorator should only return decorators existing in the scope', t => {
  t.plan(9)

  function assertsThrowOnUndeclaredDecorator (notDecorated, instanceType) {
    try {
      notDecorated.getDecorator('foo')
      t.fail()
    } catch (e) {
      t.same(e.code, 'FST_ERR_DEC_UNDECLARED')
      t.same(e.message, `No decorator 'foo' has been declared on ${instanceType}.`)
    }
  }

  const fastify = Fastify()
  fastify.register(child => {
    child.decorate('foo', true)
    child.decorateRequest('foo', true)
    child.decorateReply('foo', true)
  })

  fastify.get('/', async (req, res) => {
    assertsThrowOnUndeclaredDecorator(req, 'request')
    assertsThrowOnUndeclaredDecorator(res, 'reply')

    return { hello: 'world' }
  })

  fastify.ready((err) => {
    t.error(err)

    assertsThrowOnUndeclaredDecorator(fastify, 'instance')
    fastify.inject({ url: '/' }, (err, res) => {
      t.error(err)
      t.pass()
    })
  })
})

test('Request.setDecorator should update an existing decorator', t => {
  t.plan(7)
  const fastify = Fastify()

  fastify.decorateRequest('session', null)
  fastify.decorateRequest('utility', null)
  fastify.addHook('onRequest', async (req, reply) => {
    req.setDecorator('session', { user: 'Jean' })
    req.setDecorator('utility', function () {
      return this
    })
    try {
      req.setDecorator('foo', { user: 'Jean' })
      t.fail()
    } catch (e) {
      t.same(e.code, 'FST_ERR_DEC_UNDECLARED')
      t.same(e.message, "No decorator 'foo' has been declared on request.")
    }
  })

  fastify.get('/', async (req, res) => {
    t.strictSame(req.getDecorator('session'), { user: 'Jean' })
    t.same(req.getDecorator('utility')(), req)

    res.send()
  })

  fastify.ready((err) => {
    t.error(err)
    fastify.inject({ url: '/' }, (err, res) => {
      t.error(err)
      t.pass()
    })
  })
})
