'use strict'

const { test } = require('node:test')
const Fastify = require('..')
const { waitForCb } = require('./toolkit')

const echoBody = (req, reply) => { reply.send(req.body) }

test('basic test', (t, testDone) => {
  t.plan(3)

  const fastify = Fastify()
  fastify.get('/', {
    schema: {
      response: {
        '2xx': {
          type: 'object',
          properties: {
            name: { type: 'string' },
            work: { type: 'string' }
          }
        }
      }
    }
  }, function (req, reply) {
    reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' })
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), { name: 'Foo', work: 'Bar' })
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})

test('custom serializer options', (t, testDone) => {
  t.plan(3)

  const fastify = Fastify({
    serializerOpts: {
      rounding: 'ceil'
    }
  })
  fastify.get('/', {
    schema: {
      response: {
        '2xx': {
          type: 'integer'
        }
      }
    }
  }, function (req, reply) {
    reply.send(4.2)
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, '5', 'it must use the ceil rounding')
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})

test('Different content types', (t, testDone) => {
  t.plan(46)

  const fastify = Fastify()
  fastify.addSchema({
    $id: 'test',
    type: 'object',
    properties: {
      name: { type: 'string' },
      age: { type: 'number' },
      verified: { type: 'boolean' }
    }
  })

  fastify.get('/', {
    schema: {
      response: {
        200: {
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  name: { type: 'string' },
                  image: { type: 'string' },
                  address: { type: 'string' }
                }
              }
            },
            'application/vnd.v1+json': {
              schema: {
                type: 'array',
                items: { $ref: 'test' }
              }
            }
          }
        },
        201: {
          content: {
            '*/*': {
              schema: { type: 'string' }
            }
          }
        },
        202: {
          content: {
            '*/*': {
              schema: { const: 'Processing exclusive content' }
            }
          }
        },
        '3xx': {
          content: {
            'application/vnd.v2+json': {
              schema: {
                type: 'object',
                properties: {
                  fullName: { type: 'string' },
                  phone: { type: 'string' }
                }
              }
            }
          }
        },
        '4xx': {
          content: {
            '*/*': {
              schema: {
                type: 'object',
                properties: {
                  details: { type: 'string' }
                }
              }
            }
          }
        },
        default: {
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  details: { type: 'string' }
                }
              }
            },
            '*/*': {
              schema: {
                type: 'object',
                properties: {
                  desc: { type: 'string' },
                  details: { type: 'string' }
                }
              }
            }
          }
        }
      }
    }
  }, function (req, reply) {
    switch (req.headers.accept) {
      case 'application/json':
        reply.header('Content-Type', 'application/json')
        reply.send({ id: 1, name: 'Foo', image: 'profile picture', address: 'New Node' })
        break
      case 'application/vnd.v1+json':
        reply.header('Content-Type', 'application/vnd.v1+json')
        reply.send([{ id: 2, name: 'Boo', age: 18, verified: false }, { id: 3, name: 'Woo', age: 30, verified: true }])
        break
      case 'application/vnd.v2+json':
        reply.header('Content-Type', 'application/vnd.v2+json')
        reply.code(300)
        reply.send({ fullName: 'Jhon Smith', phone: '01090000000', authMethod: 'google' })
        break
      case 'application/vnd.v3+json':
        reply.header('Content-Type', 'application/vnd.v3+json')
        reply.code(300)
        reply.send({ firstName: 'New', lastName: 'Hoo', country: 'eg', city: 'node' })
        break
      case 'application/vnd.v4+json':
        reply.header('Content-Type', 'application/vnd.v4+json')
        reply.code(201)
        reply.send({ boxId: 1, content: 'Games' })
        break
      case 'application/vnd.v5+json':
        reply.header('Content-Type', 'application/vnd.v5+json')
        reply.code(202)
        reply.send({ content: 'interesting content' })
        break
      case 'application/vnd.v6+json':
        reply.header('Content-Type', 'application/vnd.v6+json')
        reply.code(400)
        reply.send({ desc: 'age is missing', details: 'validation error' })
        break
      case 'application/vnd.v7+json':
        reply.code(400)
        reply.send({ details: 'validation error' })
        break
      case 'application/vnd.v8+json':
        reply.header('Content-Type', 'application/vnd.v8+json')
        reply.code(500)
        reply.send({ desc: 'age is missing', details: 'validation error' })
        break
      case 'application/vnd.v9+json':
        reply.code(500)
        reply.send({ details: 'validation error' })
        break
      default:
        // to test if schema not found
        reply.header('Content-Type', 'application/vnd.v3+json')
        reply.code(200)
        reply.send([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }])
    }
  })

  fastify.get('/test', {
    serializerCompiler: ({ contentType }) => {
      t.assert.strictEqual(contentType, 'application/json')
      return data => JSON.stringify(data)
    },
    schema: {
      response: {
        200: {
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  name: { type: 'string' },
                  image: { type: 'string' },
                  address: { type: 'string' }
                }
              }
            }
          }
        },
        default: {
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  details: { type: 'string' }
                }
              }
            }
          }
        }
      }
    }
  }, function (req, reply) {
    switch (req.headers['code']) {
      case '200': {
        reply.header('Content-Type', 'application/json')
        reply.code(200).send({ age: 18, city: 'AU' })
        break
      }
      case '201': {
        reply.header('Content-Type', 'application/json')
        reply.code(201).send({ details: 'validation error' })
        break
      }
      default: {
        reply.header('Content-Type', 'application/vnd.v1+json')
        reply.code(201).send({ created: true })
        break
      }
    }
  })

  const completion = waitForCb({ steps: 14 })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ name: 'Foo', image: 'profile picture', address: 'New Node' }))
    t.assert.strictEqual(res.statusCode, 200)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v1+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify([{ name: 'Boo', age: 18, verified: false }, { name: 'Woo', age: 30, verified: true }]))
    t.assert.strictEqual(res.statusCode, 200)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/' }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify([{ type: 'student', grade: 6 }, { type: 'student', grade: 9 }]))
    t.assert.strictEqual(res.statusCode, 200)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v2+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ fullName: 'Jhon Smith', phone: '01090000000' }))
    t.assert.strictEqual(res.statusCode, 300)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v3+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ firstName: 'New', lastName: 'Hoo', country: 'eg', city: 'node' }))
    t.assert.strictEqual(res.statusCode, 300)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v4+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, '"[object Object]"')
    t.assert.strictEqual(res.statusCode, 201)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v5+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, '"Processing exclusive content"')
    t.assert.strictEqual(res.statusCode, 202)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v6+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ details: 'validation error' }))
    t.assert.strictEqual(res.statusCode, 400)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v7+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ details: 'validation error' }))
    t.assert.strictEqual(res.statusCode, 400)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v8+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ desc: 'age is missing', details: 'validation error' }))
    t.assert.strictEqual(res.statusCode, 500)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/', headers: { Accept: 'application/vnd.v9+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ details: 'validation error' }))
    t.assert.strictEqual(res.statusCode, 500)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/test', headers: { Code: '200' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ age: 18, city: 'AU' }))
    t.assert.strictEqual(res.statusCode, 200)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/test', headers: { Code: '201' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ details: 'validation error' }))
    t.assert.strictEqual(res.statusCode, 201)
    completion.stepIn()
  })
  fastify.inject({ method: 'GET', url: '/test', headers: { Accept: 'application/vnd.v1+json' } }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ created: true }))
    t.assert.strictEqual(res.statusCode, 201)
    completion.stepIn()
  })

  completion.patience.then(testDone)
})

test('Invalid multiple content schema, throw FST_ERR_SCH_CONTENT_MISSING_SCHEMA error', (t, testDone) => {
  t.plan(3)
  const fastify = Fastify()

  fastify.get('/testInvalid', {
    schema: {
      response: {
        200: {
          content: {
            'application/json': {
              schema: {
                type: 'object',
                properties: {
                  fullName: { type: 'string' },
                  phone: { type: 'string' }
                }
              },
              example: {
                fullName: 'John Doe',
                phone: '201090243795'
              }
            },
            type: 'string'
          }
        }
      }
    }
  }, function (req, reply) {
    reply.header('Content-Type', 'application/json')
    reply.send({ fullName: 'Any name', phone: '0109001010' })
  })

  fastify.ready((err) => {
    t.assert.strictEqual(err.message, "Schema is missing for the content type 'type'")
    t.assert.strictEqual(err.statusCode, 500)
    t.assert.strictEqual(err.code, 'FST_ERR_SCH_CONTENT_MISSING_SCHEMA')
    testDone()
  })
})

test('Use the same schema id in different places', (t, testDone) => {
  t.plan(2)
  const fastify = Fastify()

  fastify.addSchema({
    $id: 'test',
    type: 'object',
    properties: {
      id: { type: 'number' }
    }
  })

  fastify.get('/:id', {
    handler (req, reply) {
      reply.send([{ id: 1 }, { id: 2 }, { what: 'is this' }])
    },
    schema: {
      response: {
        200: {
          type: 'array',
          items: { $ref: 'test' }
        }
      }
    }
  })

  fastify.inject({
    method: 'GET',
    url: '/123'
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), [{ id: 1 }, { id: 2 }, {}])
    testDone()
  })
})

test('Use shared schema and $ref with $id in response ($ref to $id)', (t, testDone) => {
  t.plan(5)
  const fastify = Fastify()

  fastify.addSchema({
    $id: 'http://foo/test',
    type: 'object',
    properties: {
      id: { type: 'number' }
    }
  })

  const complexSchema = {
    $schema: 'http://json-schema.org/draft-07/schema#',
    $id: 'http://foo/user',
    type: 'object',
    definitions: {
      address: {
        $id: '#address',
        type: 'object',
        properties: {
          city: { type: 'string' }
        }
      }
    },
    properties: {
      test: { $ref: 'http://foo/test#' },
      address: { $ref: '#address' }
    },
    required: ['address', 'test']
  }

  fastify.post('/', {
    schema: {
      body: complexSchema,
      response: {
        200: complexSchema
      }
    },
    handler: (req, reply) => {
      req.body.removeThis = 'it should not be serialized'
      reply.send(req.body)
    }
  })

  const payload = {
    address: { city: 'New Node' },
    test: { id: Date.now() }
  }

  const completion = waitForCb({ steps: 2 })
  fastify.inject({
    method: 'POST',
    url: '/',
    payload
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), payload)
    completion.stepIn()
  })
  fastify.inject({
    method: 'POST',
    url: '/',
    payload: { test: { id: Date.now() } }
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.statusCode, 400)
    t.assert.deepStrictEqual(res.json(), {
      error: 'Bad Request',
      message: "body must have required property 'address'",
      statusCode: 400,
      code: 'FST_ERR_VALIDATION'
    })
    completion.stepIn()
  })

  completion.patience.then(testDone)
})

test('Shared schema should be pass to serializer and validator ($ref to shared schema /definitions)', (t, testDone) => {
  t.plan(5)
  const fastify = Fastify()

  fastify.addSchema({
    $id: 'http://example.com/asset.json',
    $schema: 'http://json-schema.org/draft-07/schema#',
    title: 'Physical Asset',
    description: 'A generic representation of a physical asset',
    type: 'object',
    required: [
      'id',
      'model',
      'location'
    ],
    properties: {
      id: {
        type: 'string',
        format: 'uuid'
      },
      model: {
        type: 'string'
      },
      location: { $ref: 'http://example.com/point.json#' }
    },
    definitions: {
      inner: {
        $id: '#innerId',
        type: 'string',
        format: 'email'
      }
    }
  })

  fastify.addSchema({
    $id: 'http://example.com/point.json',
    $schema: 'http://json-schema.org/draft-07/schema#',
    title: 'Longitude and Latitude Values',
    description: 'A geographical coordinate.',
    type: 'object',
    required: [
      'latitude',
      'longitude'
    ],
    properties: {
      email: { $ref: 'http://example.com/asset.json#/definitions/inner' },
      latitude: {
        type: 'number',
        minimum: -90,
        maximum: 90
      },
      longitude: {
        type: 'number',
        minimum: -180,
        maximum: 180
      },
      altitude: {
        type: 'number'
      }
    }
  })

  const schemaLocations = {
    $id: 'http://example.com/locations.json',
    $schema: 'http://json-schema.org/draft-07/schema#',
    title: 'List of Asset locations',
    type: 'array',
    items: { $ref: 'http://example.com/asset.json#' }
  }

  fastify.post('/', {
    schema: {
      body: schemaLocations,
      response: { 200: schemaLocations }
    }
  }, (req, reply) => {
    reply.send(locations.map(_ => Object.assign({ serializer: 'remove me' }, _)))
  })

  const locations = [
    { id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } },
    { id: '550e8400-e29b-41d4-a716-446655440000', model: 'mod', location: { latitude: 10, longitude: 10, email: 'foo@bar.it' } }
  ]
  fastify.inject({
    method: 'POST',
    url: '/',
    payload: locations
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), locations)

    fastify.inject({
      method: 'POST',
      url: '/',
      payload: locations.map(_ => {
        _.location.email = 'not an email'
        return _
      })
    }, (err, res) => {
      t.assert.ifError(err)
      t.assert.strictEqual(res.statusCode, 400)
      t.assert.deepStrictEqual(res.json(), {
        error: 'Bad Request',
        message: 'body/0/location/email must match format "email"',
        statusCode: 400,
        code: 'FST_ERR_VALIDATION'
      })
      testDone()
    })
  })
})

test('Custom setSerializerCompiler', (t, testDone) => {
  t.plan(7)
  const fastify = Fastify({ exposeHeadRoutes: false })

  const outSchema = {
    $id: 'test',
    type: 'object',
    whatever: 'need to be parsed by the custom serializer'
  }

  fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
    t.assert.strictEqual(method, 'GET')
    t.assert.strictEqual(url, '/foo/:id')
    t.assert.strictEqual(httpStatus, '200')
    t.assert.deepStrictEqual(schema, outSchema)
    return data => JSON.stringify(data)
  })

  fastify.register((instance, opts, done) => {
    instance.get('/:id', {
      handler (req, reply) {
        reply.send({ id: 1 })
      },
      schema: {
        response: {
          200: outSchema
        }
      }
    })
    t.assert.ok(instance.serializerCompiler, 'the serializer is set by the parent')
    done()
  }, { prefix: '/foo' })

  fastify.inject({
    method: 'GET',
    url: '/foo/123'
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ id: 1 }))
    testDone()
  })
})

test('Custom setSerializerCompiler returns bad serialized output', (t, testDone) => {
  t.plan(4)
  const fastify = Fastify()

  const outSchema = {
    $id: 'test',
    type: 'object',
    whatever: 'need to be parsed by the custom serializer'
  }

  fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
    return data => {
      t.assert.ok('returning an invalid serialization')
      return { not: 'a string' }
    }
  })

  fastify.get('/:id', {
    handler (req, reply) { throw new Error('ops') },
    schema: {
      response: {
        500: outSchema
      }
    }
  })

  fastify.inject({
    method: 'GET',
    url: '/123'
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.statusCode, 500)
    t.assert.deepStrictEqual(res.json(), {
      code: 'FST_ERR_REP_INVALID_PAYLOAD_TYPE',
      message: 'Attempted to send payload of invalid type \'object\'. Expected a string or Buffer.',
      statusCode: 500
    })
    testDone()
  })
})

test('Custom setSerializerCompiler with addSchema', (t, testDone) => {
  t.plan(6)
  const fastify = Fastify({ exposeHeadRoutes: false })

  const outSchema = {
    $id: 'test',
    type: 'object',
    whatever: 'need to be parsed by the custom serializer'
  }

  fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
    t.assert.strictEqual(method, 'GET')
    t.assert.strictEqual(url, '/foo/:id')
    t.assert.strictEqual(httpStatus, '200')
    t.assert.deepStrictEqual(schema, outSchema)
    return _data => JSON.stringify({ id: 2 })
  })

  // provoke re-creation of serialization compiler in setupSerializer
  fastify.addSchema({ $id: 'dummy', type: 'object' })

  fastify.get('/foo/:id', {
    handler (_req, reply) {
      reply.send({ id: 1 })
    },
    schema: {
      response: {
        200: outSchema
      }
    }
  })

  fastify.inject({
    method: 'GET',
    url: '/foo/123'
  }, (err, res) => {
    t.assert.ifError(err)
    t.assert.strictEqual(res.payload, JSON.stringify({ id: 2 }))
    testDone()
  })
})

test('Custom serializer per route', async t => {
  const fastify = Fastify()

  const outSchema = {
    $id: 'test',
    type: 'object',
    properties: {
      mean: { type: 'string' }
    }
  }

  fastify.get('/default', {
    handler (req, reply) { reply.send({ mean: 'default' }) },
    schema: { response: { 200: outSchema } }
  })

  let hit = 0
  fastify.register((instance, opts, done) => {
    instance.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
      hit++
      return data => JSON.stringify({ mean: 'custom' })
    })
    instance.get('/custom', {
      handler (req, reply) { reply.send({}) },
      schema: { response: { 200: outSchema } }
    })
    instance.get('/route', {
      handler (req, reply) { reply.send({}) },
      serializerCompiler: ({ schema, method, url, httpPart }) => {
        hit++
        return data => JSON.stringify({ mean: 'route' })
      },
      schema: { response: { 200: outSchema } }
    })

    done()
  })

  let res = await fastify.inject('/default')
  t.assert.strictEqual(res.json().mean, 'default')

  res = await fastify.inject('/custom')
  t.assert.strictEqual(res.json().mean, 'custom')

  res = await fastify.inject('/route')
  t.assert.strictEqual(res.json().mean, 'route')

  t.assert.strictEqual(hit, 4, 'the custom and route serializer has been called')
})

test('Reply serializer win over serializer ', (t, testDone) => {
  t.plan(6)

  const fastify = Fastify()
  fastify.setReplySerializer(function (payload, statusCode) {
    t.assert.deepStrictEqual(payload, { name: 'Foo', work: 'Bar', nick: 'Boo' })
    return 'instance serializator'
  })

  fastify.get('/', {
    schema: {
      response: {
        '2xx': {
          type: 'object',
          properties: {
            name: { type: 'string' },
            work: { type: 'string' }
          }
        }
      }
    },
    serializerCompiler: ({ schema, method, url, httpPart }) => {
      t.assert.ok(method, 'the custom compiler has been created')
      return () => {
        t.assert.fail('the serializer must not be called when there is a reply serializer')
        return 'fail'
      }
    }
  }, function (req, reply) {
    reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' })
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.payload, 'instance serializator')
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})

test('Reply serializer win over serializer ', (t, testDone) => {
  t.plan(6)

  const fastify = Fastify()
  fastify.setReplySerializer(function (payload, statusCode) {
    t.assert.deepStrictEqual(payload, { name: 'Foo', work: 'Bar', nick: 'Boo' })
    return 'instance serializator'
  })

  fastify.get('/', {
    schema: {
      response: {
        '2xx': {
          type: 'object',
          properties: {
            name: { type: 'string' },
            work: { type: 'string' }
          }
        }
      }
    },
    serializerCompiler: ({ schema, method, url, httpPart }) => {
      t.assert.ok(method, 'the custom compiler has been created')
      return () => {
        t.assert.fail('the serializer must not be called when there is a reply serializer')
        return 'fail'
      }
    }
  }, function (req, reply) {
    reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' })
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.payload, 'instance serializator')
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})

test('The schema compiler recreate itself if needed', (t, testDone) => {
  t.plan(1)
  const fastify = Fastify()

  fastify.options('/', {
    schema: {
      response: { '2xx': { hello: { type: 'string' } } }
    }
  }, echoBody)

  fastify.register(function (fastify, options, done) {
    fastify.addSchema({
      $id: 'identifier',
      type: 'string',
      format: 'uuid'
    })

    fastify.get('/', {
      schema: {
        response: {
          '2xx': {
            foobarId: { $ref: 'identifier#' }
          }
        }
      }
    }, echoBody)

    done()
  })

  fastify.ready(err => {
    t.assert.ifError(err)
    testDone()
  })
})

test('The schema changes the default error handler output', async t => {
  t.plan(4)
  const fastify = Fastify()

  fastify.get('/:code', {
    schema: {
      response: {
        '2xx': { hello: { type: 'string' } },
        501: {
          type: 'object',
          properties: {
            message: { type: 'string' }
          }
        },
        '5xx': {
          type: 'object',
          properties: {
            customId: { type: 'number' },
            error: { type: 'string' },
            message: { type: 'string' }
          }
        }
      }
    }
  }, (request, reply) => {
    if (request.params.code === '501') {
      return reply.code(501).send(new Error('501 message'))
    }
    const error = new Error('500 message')
    error.customId = 42
    reply.send(error)
  })

  let res = await fastify.inject('/501')
  t.assert.strictEqual(res.statusCode, 501)
  t.assert.deepStrictEqual(res.json(), { message: '501 message' })

  res = await fastify.inject('/500')
  t.assert.strictEqual(res.statusCode, 500)
  t.assert.deepStrictEqual(res.json(), { error: 'Internal Server Error', message: '500 message', customId: 42 })
})

test('do not crash if status code serializer errors', async t => {
  const fastify = Fastify()

  const requiresFoo = {
    type: 'object',
    properties: { foo: { type: 'string' } },
    required: ['foo']
  }

  const someUserErrorType2 = {
    type: 'object',
    properties: {
      customCode: { type: 'number' }
    },
    required: ['customCode']
  }

  fastify.get(
    '/',
    {
      schema: {
        query: requiresFoo,
        response: { 400: someUserErrorType2 }
      }
    },
    (request, reply) => {
      t.assert.fail('handler, should not be called')
    }
  )

  const res = await fastify.inject({
    path: '/',
    query: {
      notfoo: true
    }
  })
  t.assert.strictEqual(res.statusCode, 500)
  t.assert.deepStrictEqual(res.json(), {
    statusCode: 500,
    code: 'FST_ERR_FAILED_ERROR_SERIALIZATION',
    message: 'Failed to serialize an error. Error: "customCode" is required!. ' +
      'Original error: querystring must have required property \'foo\''
  })
})

test('custom schema serializer error, empty message', async t => {
  t.plan(2)
  const fastify = Fastify()

  fastify.get('/:code', {
    schema: {
      response: {
        '2xx': { hello: { type: 'string' } },
        501: {
          type: 'object',
          properties: {
            message: { type: 'string' }
          }
        }
      }
    }
  }, (request, reply) => {
    if (request.params.code === '501') {
      return reply.code(501).send(new Error(''))
    }
  })

  const res = await fastify.inject('/501')
  t.assert.strictEqual(res.statusCode, 501)
  t.assert.deepStrictEqual(res.json(), { message: '' })
})

test('error in custom schema serialize compiler, throw FST_ERR_SCH_SERIALIZATION_BUILD error', (t, testDone) => {
  t.plan(3)

  const fastify = Fastify()

  fastify.get('/', {
    schema: {
      response: {
        '2xx': {
          type: 'object',
          properties: {
            some: { type: 'string' }
          }
        },
        500: {
          type: 'object',
          properties: {
            message: { type: 'string' }
          }
        }
      }
    },
    serializerCompiler: () => {
      throw new Error('CUSTOM_ERROR')
    }
  }, function (req, reply) {
    reply.code(200).send({ some: 'thing' })
  })

  fastify.ready((err) => {
    t.assert.strictEqual(err.message, 'Failed building the serialization schema for GET: /, due to error CUSTOM_ERROR')
    t.assert.strictEqual(err.statusCode, 500)
    t.assert.strictEqual(err.code, 'FST_ERR_SCH_SERIALIZATION_BUILD')
    testDone()
  })
})

test('Errors in serializer send to errorHandler', async t => {
  let savedError

  const fastify = Fastify()
  fastify.get('/', {
    schema: {
      response: {
        200: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            power: { type: 'string' }
          },
          required: ['name']
        }
      }
    }

  }, function (req, reply) {
    reply.code(200).send({ no: 'thing' })
  })
  fastify.setErrorHandler((error, request, reply) => {
    savedError = error
    reply.code(500).send(error)
  })

  const res = await fastify.inject('/')

  t.assert.strictEqual(res.statusCode, 500)

  // t.assert.deepStrictEqual(savedError, new Error('"name" is required!'));
  t.assert.deepStrictEqual(res.json(), {
    statusCode: 500,
    error: 'Internal Server Error',
    message: '"name" is required!'
  })
  t.assert.ok(savedError, 'error presents')
  t.assert.ok(savedError.serialization, 'Serialization sign presents')
})

test('capital X', (t, testDone) => {
  t.plan(3)

  const fastify = Fastify()
  fastify.get('/', {
    schema: {
      response: {
        '2XX': {
          type: 'object',
          properties: {
            name: { type: 'string' },
            work: { type: 'string' }
          }
        }
      }
    }
  }, function (req, reply) {
    reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' })
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), { name: 'Foo', work: 'Bar' })
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})

test('allow default as status code and used as last fallback', (t, testDone) => {
  t.plan(3)
  const fastify = Fastify()

  fastify.route({
    url: '/',
    method: 'GET',
    schema: {
      response: {
        default: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            work: { type: 'string' }
          }
        }
      }
    },
    handler: (req, reply) => {
      reply.code(200).send({ name: 'Foo', work: 'Bar', nick: 'Boo' })
    }
  })

  fastify.inject('/', (err, res) => {
    t.assert.ifError(err)
    t.assert.deepStrictEqual(res.json(), { name: 'Foo', work: 'Bar' })
    t.assert.strictEqual(res.statusCode, 200)
    testDone()
  })
})
