'use strict'

const test = require('tap').test
const FormData = require('form-data')
const Fastify = require('fastify')
const multipart = require('..')
const http = require('node:http')

test('should parse JSON fields forms if content-type is set', function (t) {
  t.plan(5)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart)

  fastify.post('/', async function (req, reply) {
    for await (const part of req.parts()) {
      t.notOk(part.filename)
      t.equal(part.mimetype, 'application/json')
      t.same(part.value, { a: 'b' })
    }

    reply.code(200).send()
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 200)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('json', JSON.stringify({ a: 'b' }), { contentType: 'application/json' })
    form.pipe(req)
  })
})

test('should not parse JSON fields forms if no content-type is set', function (t) {
  t.plan(5)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart)

  fastify.post('/', async function (req, reply) {
    for await (const part of req.parts()) {
      t.notOk(part.filename)
      t.equal(part.mimetype, 'text/plain')
      t.type(part.value, 'string')
    }

    reply.code(200).send()
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 200)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('json', JSON.stringify({ a: 'b' }))

    form.pipe(req)
  })
})

test('should not parse JSON fields forms if non-json content-type is set', function (t) {
  t.plan(5)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart)

  fastify.post('/', async function (req, reply) {
    for await (const part of req.parts()) {
      t.notOk(part.filename)
      t.equal(part.mimetype, 'text/css')
      t.type(part.value, 'string')
    }

    reply.code(200).send()
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 200)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('css', 'body { width: 100% }', { contentType: 'text/css' })

    form.pipe(req)
  })
})

test('should throw error when parsing JSON fields failed', function (t) {
  t.plan(2)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart)

  fastify.post('/', async function (req, reply) {
    try {
      for await (const part of req.parts()) {
        t.type(part.value, 'string')
      }

      reply.code(200).send()
    } catch (error) {
      t.ok(error instanceof fastify.multipartErrors.InvalidJSONFieldError)
      reply.code(500).send()
    }
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 500)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('object', 'INVALID', { contentType: 'application/json' })
    form.pipe(req)
  })
})

test('should always reject JSON parsing if the value was truncated', function (t) {
  t.plan(2)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart, { limits: { fieldSize: 2 } })

  fastify.post('/', async function (req, reply) {
    try {
      for await (const part of req.parts()) {
        t.type(part.value, 'string')
      }

      reply.code(200).send()
    } catch (error) {
      t.ok(error instanceof fastify.multipartErrors.InvalidJSONFieldError)
      reply.code(500).send()
    }
  })

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 500)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('object', JSON.stringify({ a: 'b' }), { contentType: 'application/json' })
    form.pipe(req)
  })
})

test('should be able to use JSON schema to validate request when value is a string', function (t) {
  t.plan(5)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart, { attachFieldsToBody: true, sharedSchemaId: '#mySharedSchema' })

  fastify.post(
    '/',
    {
      schema: {
        body: {
          type: 'object',
          required: ['field'],
          properties: {
            field: {
              allOf: [{ $ref: '#mySharedSchema' }, {
                type: 'object',
                properties: { value: { type: 'string' } }
              }]
            }
          }
        }
      }
    },
    async function (req, reply) {
      t.ok(req.isMultipart())

      t.same(Object.keys(req.body), ['field'])
      t.equal(req.body.field.value, '{"a":"b"}')

      reply.code(200).send()
    }
  )

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 200)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('field', JSON.stringify({ a: 'b' }))
    form.pipe(req)
  })
})

test('should be able to use JSON schema to validate request when value is a JSON', function (t) {
  t.plan(5)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart, { attachFieldsToBody: true, sharedSchemaId: '#mySharedSchema' })

  fastify.post(
    '/',
    {
      schema: {
        body: {
          type: 'object',
          required: ['field'],
          properties: {
            field: {
              allOf: [{ $ref: '#mySharedSchema' }, { type: 'object', properties: { value: { type: 'object' } } }]
            }
          }
        }
      }
    },
    async function (req, reply) {
      t.ok(req.isMultipart())

      t.same(Object.keys(req.body), ['field'])
      t.same(req.body.field.value, { a: 'b' })

      reply.code(200).send()
    }
  )

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 200)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('field', JSON.stringify({ a: 'b' }), { contentType: 'application/json' })
    form.pipe(req)
  })
})

test('should return 400 when the field validation fails', function (t) {
  t.plan(2)

  const fastify = Fastify()
  t.teardown(fastify.close.bind(fastify))

  fastify.register(multipart, { attachFieldsToBody: true, sharedSchemaId: '#mySharedSchema' })

  fastify.post(
    '/',
    {
      schema: {
        body: {
          type: 'object',
          required: ['field'],
          properties: {
            field: {
              allOf: [{ $ref: '#mySharedSchema' }, { type: 'object', properties: { value: { type: 'object' } } }]
            }
          }
        }
      }
    },
    async function (req, reply) {
      t.ok(req.isMultipart())
      reply.code(200).send()
    }
  )

  fastify.listen({ port: 0 }, async function () {
    // request
    const form = new FormData()
    const opts = {
      protocol: 'http:',
      hostname: 'localhost',
      port: fastify.server.address().port,
      path: '/',
      headers: form.getHeaders(),
      method: 'POST'
    }

    const req = http.request(opts, res => {
      t.equal(res.statusCode, 400)
      res.resume()
      res.on('end', () => {
        t.pass('res ended successfully')
      })
    })

    form.append('field', JSON.stringify('abc'), { contentType: 'application/json' })
    form.pipe(req)
  })
})
