import { test } from '@japa/runner'
import net from 'net'
import {
  DummyLogger,
  CoreEngine,
  IHttpRequest,
  HttpEngineOptions,
  RequestAuthorizationKind,
  Headers,
  PayloadSerializer,
} from '../../../../src/index.js'

const logger = new DummyLogger()

test.group('_prepareMessage()', () => {
  test('returns a buffer', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])),
    }
    const opts: HttpEngineOptions = {
      timeout: 50000,
      followRedirects: false,
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers(''))
    assert.isTrue(result instanceof Buffer)
  })

  test('contains the status message', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])),
    }
    const opts: HttpEngineOptions = {
      timeout: 50000,
      followRedirects: false,
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers('')).toString()
    assert.equal(result.split('\n')[0], 'POST /api/endpoint?query=param HTTP/1.1\r')
  })

  test('removes hash from the URL', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param#access_token=test`,
      method: 'GET',
      headers: 'Host: test.com',
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers('')).toString()
    assert.equal(result.split('\n')[0], 'GET /api/endpoint?query=param HTTP/1.1\r')
  })

  test('adds the Host header', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])),
    }
    const opts: HttpEngineOptions = {
      timeout: 50000,
      followRedirects: false,
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers('')).toString()
    assert.equal(result.split('\n')[1], `Host: localhost:${httpConfig.httpPort}\r`)
  })

  test('adds the passed headers', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])),
    }
    const opts: HttpEngineOptions = {
      timeout: 50000,
      followRedirects: false,
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers('content-type: text/plain')).toString()
    assert.equal(result.split('\n')[2], 'content-type: text/plain\r')
  })

  test('adds empty line after headers', ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])),
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = engine._prepareMessage(new Headers('content-type: text/plain')).toString()
    assert.equal(result.split('\n')[3], '\r')
  })

  test('adds the payload message', ({ assert, httpConfig }) => {
    const payloadBuffer = Buffer.from([0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74])
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(payloadBuffer),
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const headers = new Headers('content-type: text/plain')
    const result = engine._prepareMessage(headers, payloadBuffer).toString()
    assert.equal(result.split('\n')[4], 'test')
    assert.equal(result.split('\n')[5], 'test')
  })

  test('encodes query parameters', ({ assert, httpConfig }) => {
    const request = {
      url: `http://localhost:${httpConfig.httpPort}/v1/query-params?va=test-paßword`,
      method: 'GET',
      headers: '',
    }
    const engine = new CoreEngine(request, { logger })
    const result = engine._prepareMessage(new Headers('')).toString()
    assert.include(result, 'GET /v1/query-params?va=test-pa%C3%9Fword HTTP/1.1')
  })
})

test.group('prepareMessage()', () => {
  const bytes = [0x74, 0x65, 0x73, 0x74, 0x0a, 0x74, 0x65, 0x73, 0x74]

  test('returns a Buffer', async ({ assert, httpConfig }) => {
    const request = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'GET',
      headers: 'Host: test.com\nContent-Length: 0',
      payload: 'abc',
    }
    const engine = new CoreEngine(request, { logger })
    const result = await engine.prepareMessage()
    assert.isTrue(result instanceof Buffer)
  })

  test('ignores payload for GET requests', async ({ assert, httpConfig }) => {
    const request = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'GET',
      headers: 'Host: test.com\nContent-Length: 0',
      payload: 'abc',
    }
    const engine = new CoreEngine(request, { logger })
    const result = await engine.prepareMessage()
    assert.lengthOf(result.toString().split('\n'), 5)
  })

  test('creates a message with the passed payload', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
    }
    const opts: HttpEngineOptions = {
      timeout: 50000,
      followRedirects: false,
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = await engine.prepareMessage()
    assert.lengthOf(result.toString().split('\n'), 7)
  })

  test('adds NTLM request headers from payload processing', async ({ assert, httpConfig }) => {
    const request = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'GET',
      headers: 'Host: test.com',
    }
    const opts: HttpEngineOptions = {
      logger,
      authorization: [
        {
          kind: RequestAuthorizationKind,
          type: 'ntlm',
          enabled: true,
          valid: true,
          config: {
            domain: 'domain.com',
            username: 'test',
            password: 'test',
          },
        },
      ],
    }

    const engine = new CoreEngine(request, opts)
    const result = await engine.prepareMessage()
    const headers = engine.request.headers as string
    assert.equal(headers.indexOf('NTLM '), -1, 'Headers are not altered')
    assert.isAbove(result.toString().indexOf('NTLM '), 0, 'Adds headers to body')
  })

  test('adds content length header', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    const result = await engine.prepareMessage()
    const headers = engine.sentRequest.headers as string
    const search = headers.indexOf('content-length: 9')
    assert.isAbove(search, 0)
    assert.isAbove(result.toString().indexOf('content-length: 9'), 0)
  })

  test('adds the default headers', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
    }
    const opts: HttpEngineOptions = {
      logger,
      defaultHeaders: true,
    }
    const engine = new CoreEngine(request, opts)
    await engine.prepareMessage()

    assert.include(engine.sentRequest.headers, 'user-agent: api client', 'user-agent is set')
    assert.include(engine.sentRequest.headers, 'accept: */*', 'accept is set')
  })

  test('adds missing content-type from payloadOptions', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: '',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
      payloadOptions: {
        mime: 'test/text',
      },
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    await engine.prepareMessage()
    assert.include(engine.sentRequest.headers, 'content-type: test/text')
  })

  test('does not add missing content-type when payloadOptions is not set', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: '',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    await engine.prepareMessage()
    assert.notInclude(engine.sentRequest.headers, 'content-type')
  })

  test('does not override content-type with payloadOptions when has the header', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'POST',
      headers: 'content-type: text/plain',
      payload: PayloadSerializer.stringifyBuffer(Buffer.from([...bytes])),
      payloadOptions: {
        mime: 'test/text',
      },
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    await engine.prepareMessage()
    assert.include(engine.sentRequest.headers, 'content-type: text/plain')
  })

  test('does not add content-type for non-payload requests', async ({ assert, httpConfig }) => {
    const request: IHttpRequest = {
      url: `http://localhost:${httpConfig.httpPort}/api/endpoint?query=param`,
      method: 'GET',
      headers: '',
      payloadOptions: {
        mime: 'test/text',
      },
    }
    const opts: HttpEngineOptions = {
      logger,
    }
    const engine = new CoreEngine(request, opts)
    await engine.prepareMessage()
    assert.notInclude(engine.sentRequest.headers, 'content-type')
  })
})

test.group('writeMessage()', (group) => {
  let message: Buffer
  let engine: CoreEngine
  let createdClient: net.Socket | undefined

  group.setup(() => {
    let str = 'GET /api/endpoint?query=param HTTP/1.1\r\n'
    str += 'Host: localhost:8123\r\n'
    str += '\r\n'
    message = Buffer.from(str)
  })

  group.each.setup(async (ctx) => {
    const request: IHttpRequest = {
      url: `http://localhost:${ctx.context.httpConfig.httpPort}/v1/stream`,
      method: 'GET',
      headers: 'Host: test.com\nContent-Length: 0',
      payload: 'abc',
    }
    const opts: HttpEngineOptions = {
      logger,
    }

    engine = new CoreEngine(request, opts)
    createdClient = await engine.connect()
  })

  group.each.teardown(() => {
    if (createdClient) {
      createdClient.end()
      createdClient.destroy()
      createdClient = undefined
    }
  })

  test('sets the messageSent property on the sentRequest', async ({ assert }) => {
    await engine.writeMessage(message)
    assert.typeOf(engine.sentRequest.httpMessage, 'string')
  })

  test('sets the messageStart property on the stats object', async ({ assert }) => {
    await engine.writeMessage(message)
    assert.typeOf(engine.stats.messageStart, 'number')
  })

  test('sets the sentTime property on stats object', async ({ assert }) => {
    await engine.writeMessage(message)
    assert.typeOf(engine.stats.sentTime, 'number')
  })

  test('emits the "loadstart" event', async ({ assert }) => {
    const loadstartPromise = new Promise<void>((resolve) => {
      engine.once('loadstart', () => {
        resolve()
      })
    })
    engine.writeMessage(message)
    await loadstartPromise
    assert.isTrue(true)
  })
})
