'use strict'

const { test, after, describe } = require('node:test')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { MockAgent, MockClient, getGlobalDispatcher, setGlobalDispatcher, request } = require('..')
const { kUrl } = require('../lib/core/symbols')
const { kDispatches } = require('../lib/mock/mock-symbols')
const { InvalidArgumentError } = require('../lib/core/errors')
const { MockInterceptor } = require('../lib/mock/mock-interceptor')
const { getResponse } = require('../lib/mock/mock-utils')
const Dispatcher = require('../lib/dispatcher/dispatcher')

describe('MockClient - constructor', () => {
  test('fails if opts.agent does not implement `get` method', t => {
    t.plan(1)
    t.assert.throws(() => new MockClient('http://localhost:9999', { agent: { get: 'not a function' } }), InvalidArgumentError)
  })

  test('sets agent', t => {
    t.plan(1)
    t.assert.doesNotThrow(() => new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) }))
  })

  test('should implement the Dispatcher API', t => {
    t.plan(1)

    const mockClient = new MockClient('http://localhost:9999', { agent: new MockAgent({ connections: 1 }) })
    t.assert.ok(mockClient instanceof Dispatcher)
  })
})

describe('MockClient - dispatch', () => {
  test('should handle a single interceptor', (t) => {
    t.plan(1)

    const baseUrl = 'http://localhost:9999'

    const mockAgent = new MockAgent({ connections: 1 })
    after(() => mockAgent.close())

    const mockClient = mockAgent.get(baseUrl)

    this[kUrl] = new URL('http://localhost:9999')
    mockClient[kDispatches] = [
      {
        path: '/foo',
        method: 'GET',
        data: {
          statusCode: 200,
          data: 'hello',
          headers: {},
          trailers: {},
          error: null
        }
      }
    ]

    t.assert.doesNotThrow(() => mockClient.dispatch({
      path: '/foo',
      method: 'GET'
    }, {
      onRequestStart () {},
      onResponseStart () {},
      onResponseData () {},
      onResponseEnd () {},
      onResponseError () {}
    }))
  })

  test('should directly throw error from mockDispatch function if error is not a MockNotMatchedError', (t) => {
    t.plan(1)

    const baseUrl = 'http://localhost:9999'

    const mockAgent = new MockAgent({ connections: 1 })
    after(() => mockAgent.close())

    const mockClient = mockAgent.get(baseUrl)

    this[kUrl] = new URL('http://localhost:9999')
    mockClient[kDispatches] = [
      {
        path: '/foo',
        method: 'GET',
        data: {
          statusCode: 200,
          data: 'hello',
          headers: {},
          trailers: {},
          error: null
        }
      }
    ]

    t.assert.throws(() => mockClient.dispatch({
      path: '/foo',
      method: 'GET'
    }, {
      onRequestStart () {},
      onResponseStart () { throw new Error('kaboom') },
      onResponseData () {},
      onResponseEnd () {},
      onResponseError () {}
    }), new Error('kaboom'))
  })
})

test('MockClient - intercept should return a MockInterceptor', (t) => {
  t.plan(1)

  const baseUrl = 'http://localhost:9999'

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)

  const interceptor = mockClient.intercept({
    path: '/foo',
    method: 'GET'
  })

  t.assert.ok(interceptor instanceof MockInterceptor)
})

describe('MockClient - intercept validation', () => {
  test('it should error if no options specified in the intercept', t => {
    t.plan(1)
    const mockAgent = new MockAgent({ connections: 1 })
    after(() => mockAgent.close())

    const mockClient = mockAgent.get('http://localhost:9999')

    t.assert.throws(() => mockClient.intercept(), new InvalidArgumentError('opts must be an object'))
  })

  test('it should error if no path specified in the intercept', t => {
    t.plan(1)
    const mockAgent = new MockAgent({ connections: 1 })
    after(() => mockAgent.close())

    const mockClient = mockAgent.get('http://localhost:9999')

    t.assert.throws(() => mockClient.intercept({}), new InvalidArgumentError('opts.path must be defined'))
  })

  test('it should default to GET if no method specified in the intercept', t => {
    t.plan(1)
    const mockAgent = new MockAgent({ connections: 1 })
    after(() => mockAgent.close())

    const mockClient = mockAgent.get('http://localhost:9999')
    t.assert.doesNotThrow(() => mockClient.intercept({ path: '/foo' }))
  })

  test('it should uppercase the method - https://github.com/nodejs/undici/issues/1320', t => {
    t.plan(1)

    const mockAgent = new MockAgent()
    const mockClient = mockAgent.get('http://localhost:3000')

    after(() => mockAgent.close())

    mockClient.intercept({
      path: '/test',
      method: 'patch'
    }).reply(200, 'Hello!')

    t.assert.strictEqual(mockClient[kDispatches][0].method, 'PATCH')
  })
})

test('MockClient - close should run without error', async (t) => {
  t.plan(1)

  const baseUrl = 'http://localhost:9999'

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  mockClient[kDispatches] = [
    {
      path: '/foo',
      method: 'GET',
      data: {
        statusCode: 200,
        data: 'hello',
        headers: {},
        trailers: {},
        error: null
      }
    }
  ]

  await mockClient.close()
  t.assert.ok(true, 'pass')
})

test('MockClient - should be able to set as globalDispatcher', async (t) => {
  t.plan(3)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  const originalDispatcher = getGlobalDispatcher()
  setGlobalDispatcher(mockClient)
  t.after(() => setGlobalDispatcher(originalDispatcher))

  mockClient.intercept({
    path: '/foo',
    method: 'GET'
  }).reply(200, 'hello')

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET'
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})

test('MockClient - should support query params', async (t) => {
  t.plan(3)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  const query = {
    pageNum: 1
  }
  mockClient.intercept({
    path: '/foo',
    query,
    method: 'GET'
  }).reply(200, 'hello')

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET',
    dispatcher: mockClient,
    query
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})

test('MockClient - should intercept query params with hardcoded path', async (t) => {
  t.plan(3)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  const query = {
    pageNum: 1
  }
  mockClient.intercept({
    path: '/foo?pageNum=1',
    method: 'GET'
  }).reply(200, 'hello')

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET',
    dispatcher: mockClient,
    query
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})

test('MockClient - should intercept query params regardless of key ordering', async (t) => {
  t.plan(3)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  const query = {
    pageNum: 1,
    limit: 100,
    ordering: [false, true]
  }

  mockClient.intercept({
    path: '/foo',
    query: {
      ordering: query.ordering,
      pageNum: query.pageNum,
      limit: query.limit
    },
    method: 'GET'
  }).reply(200, 'hello')

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET',
    dispatcher: mockClient,
    query
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})

test('MockClient - should be able to use as a local dispatcher', async (t) => {
  t.plan(3)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  mockClient.intercept({
    path: '/foo',
    method: 'GET'
  }).reply(200, 'hello')

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET',
    dispatcher: mockClient
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})

test('MockClient - basic intercept with MockClient.request', async (t) => {
  t.plan(5)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('should not be called')
    t.assert.fail('should not be called')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())
  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  mockClient.intercept({
    path: '/foo?hello=there&see=ya',
    method: 'POST',
    body: 'form1=data1&form2=data2'
  }).reply(200, { foo: 'bar' }, {
    headers: { 'content-type': 'application/json' },
    trailers: { 'Content-MD5': 'test' }
  })

  const { statusCode, headers, trailers, body } = await mockClient.request({
    origin: baseUrl,
    path: '/foo?hello=there&see=ya',
    method: 'POST',
    body: 'form1=data1&form2=data2'
  })
  t.assert.strictEqual(statusCode, 200)
  t.assert.strictEqual(headers['content-type'], 'application/json')
  t.assert.deepStrictEqual(trailers, { 'content-md5': 'test' })

  const jsonResponse = JSON.parse(await getResponse(body))
  t.assert.deepStrictEqual(jsonResponse, {
    foo: 'bar'
  })
})

test('MockClient - cleans mocks', async (t) => {
  t.plan(4)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.setHeader('content-type', 'text/plain')
    res.end('hello')
  })
  after(() => {
    server.closeAllConnections?.()
    server.close()
  })

  await promisify(server.listen.bind(server))(0)

  const baseUrl = `http://localhost:${server.address().port}`

  const mockAgent = new MockAgent({ connections: 1 })
  after(() => mockAgent.close())

  const mockClient = mockAgent.get(baseUrl)
  t.assert.ok(mockClient instanceof MockClient)

  mockClient.intercept({
    path: '/foo',
    method: 'GET'
  }).reply(500, () => {
    t.assert.fail('should not be called')
  })

  mockClient.cleanMocks()

  t.assert.strictEqual(mockClient[kDispatches].length, 0)

  const { statusCode, body } = await request(`${baseUrl}/foo`, {
    method: 'GET',
    dispatcher: mockClient
  })
  t.assert.strictEqual(statusCode, 200)

  const response = await getResponse(body)
  t.assert.deepStrictEqual(response, 'hello')
})
