import { test } from '@japa/runner'
import {
  RequestDataExtractor,
  HttpRequestKind,
  IHttpRequest,
  HttpResponseKind,
  IHttpResponse,
  HttpResponse,
} from '../../../src/index.js'
import { FlowSourceEnum, FlowRequestDataEnum, FlowResponseDataEnum } from '../../../src/models/http-flows/HttpFlows.js'

test.group('constructor()', () => {
  test('sets the request instance', ({ assert }) => {
    const instance = new RequestDataExtractor({
      url: 'https://domain.com',
      method: 'GET',
    })
    assert.typeOf(instance.request, 'object', 'has the object')
    assert.equal(instance.request.kind, HttpRequestKind, 'has the HttpRequest instance')
  })

  test('does not set the response instance when missing', ({ assert }) => {
    const instance = new RequestDataExtractor({
      url: 'https://domain.com',
      method: 'GET',
    })
    assert.isUndefined(instance.response)
  })

  test('sets the response instance', ({ assert }) => {
    const instance = new RequestDataExtractor(
      {
        url: 'https://domain.com',
        method: 'GET',
      },
      {
        kind: HttpResponseKind,
        status: 200,
      }
    )
    assert.typeOf(instance.response, 'object', 'has the object')
    const response = instance.response as HttpResponse
    assert.equal(response.kind, HttpResponseKind, 'has the HttpResponse instance')
  })
})

test.group('URL extraction', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
    }
  })

  test('returns the whole URL when no path', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url)
    assert.equal(result, 'https://dot.com')
  })

  test('returns the "host" path', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'host')
    assert.equal(result, 'dot.com')
  })

  test('returns the "protocol" path', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'protocol')
    assert.equal(result, 'https:')
  })

  test('returns the "path" path', async ({ assert }) => {
    request.url += '/a/path/index.html'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'path')
    assert.equal(result, '/a/path/index.html')
  })

  test('returns the whole "query" path', async ({ assert }) => {
    request.url += '/a/b?c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'query')
    assert.equal(result, 'c=d&e=f')
  })

  test('returns a specific query parameter', async ({ assert }) => {
    request.url += '/a/b?c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'query.c')
    assert.equal(result, 'd')
  })

  test('returns undefined when unknown query parameter', async ({ assert }) => {
    request.url += '/a/b?c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'query.g')
    assert.isUndefined(result)
  })

  test('returns undefined when no query parameters', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'query.g')
    assert.isUndefined(result)
  })

  test('returns the whole "hash" path', async ({ assert }) => {
    request.url += '/a/b#c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'hash')
    assert.equal(result, 'c=d&e=f')
  })

  test('returns a specific hash as query parameter', async ({ assert }) => {
    request.url += '/a/b#c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'hash.c')
    assert.equal(result, 'd')
  })

  test('returns undefined when unknown hash as query parameter', async ({ assert }) => {
    request.url += '/a/b#c=d&e=f'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'hash.g')
    assert.isUndefined(result)
  })

  test('returns undefined when no hash part', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'hash.g')
    assert.isUndefined(result)
  })

  test('throws when unknown path', async ({ assert }) => {
    let thrown = false
    const factory = new RequestDataExtractor(request)
    try {
      await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.url, 'something')
    } catch {
      thrown = true
    }
    assert.isTrue(thrown)
  })
})

test.group('request headers', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      headers: 'x-t1: true\naccept: text/plain',
    }
  })

  test('returns the whole headers value when no path', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.headers)
    assert.equal(result, 'x-t1: true\naccept: text/plain')
  })

  test('returns a specific header', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.headers, 'accept')
    assert.equal(result, 'text/plain')
  })

  test('returns undefined for unknown header', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.headers, 'some')
    assert.isUndefined(result)
  })

  test('returns undefined when no headers', async ({ assert }) => {
    delete request.headers
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.headers, 'some')
    assert.isUndefined(result)
  })
})

test.group('response headers', (group) => {
  let request: IHttpRequest
  let response: IHttpResponse
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
    }
    response = {
      status: 200,
      kind: HttpResponseKind,
      headers: 'x-t1: true\naccept: text/plain',
    }
  })

  test('returns the whole headers value when no path', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.headers)
    assert.equal(result, 'x-t1: true\naccept: text/plain')
  })

  test('returns a specific header', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.headers, 'accept')
    assert.equal(result, 'text/plain')
  })

  test('returns undefined for unknown header', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.headers, 'some')
    assert.isUndefined(result)
  })

  test('returns undefined when no headers', async ({ assert }) => {
    delete request.headers
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.headers, 'some')
    assert.isUndefined(result)
  })

  test('returns undefined when no response object', async ({ assert }) => {
    delete request.headers
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.headers, 'some')
    assert.isUndefined(result)
  })
})

test.group('status extraction', (group) => {
  let request: IHttpRequest
  let response: IHttpResponse
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
    }
    response = {
      status: 200,
      kind: HttpResponseKind,
    }
  })

  test('returns the response status', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.status)
    assert.strictEqual(result, 200)
  })

  test('returns undefined when no response', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.response, FlowResponseDataEnum.status)
    assert.isUndefined(result)
  })
})

test.group('method extraction', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
    }
  })

  test('returns the http method', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.method)
    assert.strictEqual(result, 'GET')
  })
})

test.group('data source selection', (group) => {
  let request: IHttpRequest
  let response: IHttpResponse
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      payload: 'request-payload',
    }
    response = {
      status: 200,
      kind: HttpResponseKind,
      payload: 'response-payload',
    }
  })

  test('extract body from the request object', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body)
    assert.equal(result, 'request-payload')
  })

  test('extract body from the response object', async ({ assert }) => {
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowRequestDataEnum.body)
    assert.equal(result, 'response-payload')
  })

  test('returns undefined when no request payload', async ({ assert }) => {
    delete request.payload
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body)
    assert.isUndefined(result)
  })

  test('returns undefined when no response payload', async ({ assert }) => {
    delete response.payload
    const factory = new RequestDataExtractor(request, response)
    const result = await factory.extract(FlowSourceEnum.response, FlowRequestDataEnum.body)
    assert.isUndefined(result)
  })

  test('returns undefined when no response', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.response, FlowRequestDataEnum.body)
    assert.isUndefined(result)
  })
})

test.group('JSON data extraction', (group) => {
  const simpleBody = JSON.stringify({ a: { b: { c: 'value' } } })

  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      headers: 'content-length: 100\ncontent-type: application/json',
    }
  })

  test('returns undefined when invalid payload', async ({ assert }) => {
    request.payload = 'test'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'a.b.c')
    assert.isUndefined(result)
  })

  test('returns undefined when no headers', async ({ assert }) => {
    request.payload = simpleBody
    delete request.headers
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'a.b.c')
    assert.isUndefined(result)
  })

  test('returns undefined when no content-type header', async ({ assert }) => {
    request.payload = simpleBody
    request.headers = 'content-length: 100'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'a.b.c')
    assert.isUndefined(result)
  })

  test('returns the value for a simple object', async ({ assert }) => {
    request.payload = simpleBody
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'a.b.c')
    assert.equal(result, 'value')
  })

  test('returns the value with an iterator', async ({ assert }) => {
    request.payload = JSON.stringify({
      tokens: [
        { type: 'Bearer', value: 'bearer-token' },
        { type: 'openid', value: 'openid-token' },
      ],
    })
    const factory = new RequestDataExtractor(request)
    // const result = await factory.extract(FlowSourceEnum.request,
    //  FlowRequestDataEnum.body, '/tokens[type=\'Bearer\']/value[1]');
    const result = await factory.extract(
      FlowSourceEnum.request,
      FlowRequestDataEnum.body,
      "tokens[?type == 'Bearer'].value"
    )
    assert.equal(result, 'bearer-token')
  })
})

test.group('XML data extraction', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      headers: 'content-length: 100\ncontent-type: application/xml',
    }
  })

  test('returns undefined when invalid payload', async ({ assert }) => {
    request.payload = 'test'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'root/a')
    assert.isUndefined(result)
  })

  test('returns the value for a simple payload', async ({ assert }) => {
    request.payload = '<feed><item><name>Test</name></item></feed>'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'feed/item/name')
    assert.equal(result, 'Test')
  })

  test('returns the value with an iterator', async ({ assert }) => {
    request.payload = '<feed><item><name>Test</name></item><item><name>Other</name></item></feed>'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(
      FlowSourceEnum.request,
      FlowRequestDataEnum.body,
      'feed/item[name="Other"]/name'
    )
    assert.equal(result, 'Other')
  })
})

test.group('x-www-form-urlencoded data extraction', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      headers: 'content-length: 100\ncontent-type: x-www-form-urlencoded',
    }
  })

  test('returns undefined when invalid payload', async ({ assert }) => {
    request.payload = 'test'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'param')
    assert.isUndefined(result)
  })

  test('returns the value for a simple payload', async ({ assert }) => {
    request.payload = 'a=b&c=d'
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'c')
    assert.equal(result, 'd')
  })
})

test.group('other data extraction', (group) => {
  let request: IHttpRequest
  group.each.setup(() => {
    request = {
      url: 'https://dot.com',
      method: 'GET',
      headers: 'content-length: 100\ncontent-type: text/plain',
      payload: 'lorem ipsum...',
    }
  })

  test('always returns undefined for unsupported mime type', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    const result = await factory.extract(FlowSourceEnum.request, FlowRequestDataEnum.body, 'lorem')
    assert.isUndefined(result)
  })

  test('throws for unknown source', async ({ assert }) => {
    const factory = new RequestDataExtractor(request)
    let thrown = false
    try {
      // @ts-expect-error Used in testing
      await factory.extract(FlowSourceEnum.request, 'unknown')
    } catch {
      thrown = true
    }
    assert.isTrue(thrown)
  })
})
