import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { Provider } from '../../src/resources/provider.js';
import * as HttpErrors from '../../src/httpErrors.js';
import Logger from '../../src/resources/logger.js';

// There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
// get properly instantiated, which allow it to be mocked properly.
// Issue: https://github.com/nodejs/node/issues/52015
// PR fix: https://github.com/nodejs/node/pull/52275
globalThis.fetch = fetch;

describe('Provider', () => {
  const provider = new Provider({
    prepareRequest: requestOptions => {
      return {
        url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
        headers: {
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
        },
      };
    },
  });

  const logger = new Logger();

  it('get', async context => {
    const response = new Response('{"data": "value"}', {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('/endpoint', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
  });

  it('accepts text/html type response', async context => {
    const response = new Response('', {
      status: 200,
      headers: { 'Content-Type': 'text/html; charset=UTF-8' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('/endpoint', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'text/html; charset=UTF-8' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);

    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'text/html; charset=UTF-8',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);

    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' });
  });

  it('accepts application/schema+json type response', async context => {
    const response = new Response('{"data": "value"}', {
      status: 200,
      headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('/endpoint', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/schema+json; charset=UTF-8' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);

    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/schema+json; charset=UTF-8',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);

    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
  });

  it('accepts application/swagger+json type response', async context => {
    const response = new Response('{"data": "value"}', {
      status: 200,
      headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('/endpoint', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/swagger+json; charset=UTF-8' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);

    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/swagger+json; charset=UTF-8',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);

    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
  });

  it('accepts application/vnd.oracle.resource+json type response', async context => {
    const response = new Response('{"data": "value"}', {
      status: 200,
      headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('/endpoint', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);

    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);

    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
  });

  it('returns the raw response body if specified', async context => {
    const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
      status: 200,
      headers: { 'Content-Type': 'image/png' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const providerResponse = await provider.streamingGet('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: {
        Accept: 'application/json',
      },
      rawBody: true,
    });

    assert.ok(providerResponse);
    // What matters: still returns a stream
    assert.ok(providerResponse.body instanceof ReadableStream);
  });

  it('gets an endpoint which is an absolute url', async context => {
    const response = new Response('{"data": "value"}', {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.get('https://my-cdn.my-domain.com/file.png', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
    });

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'https://my-cdn.my-domain.com/file.png',
      {
        method: 'GET',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
  });

  it('post with url encoded body', async context => {
    const response = new Response('{"data": "value"}', {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.post(
      '/endpoint',
      {
        data: 'createdItemInfo',
      },
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
        additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' },
      },
    );

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'POST',
        body: 'data=createdItemInfo',
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
  });

  it('accepts an array as body for post request', async context => {
    const response = new Response('{"data": "value"}', {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.post(
      '/endpoint',
      [
        { data: '1', data2: '2' },
        { data: '3', data2: '4' },
      ],
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
        additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
      },
    );

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint',
      {
        method: 'POST',
        body: '[{"data":"1","data2":"2"},{"data":"3","data2":"4"}]',
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/json-patch+json',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
  });

  it('put with json body', async context => {
    const response = new Response('{"data": "value"}', {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    // Removing leading '/' on endpoint to make sure we support both cases
    const actualResponse = await provider.put(
      'endpoint/123',
      {
        data: 'updatedItemInfo',
      },
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
        additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
      },
    );

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint/123',
      {
        method: 'PUT',
        body: JSON.stringify({ data: 'updatedItemInfo' }),
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
  });

  it('putBuffer with Buffer body', async context => {
    const response = new Response('{"data": "value"}', {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const buffer = Buffer.from('binary data content');

    // What matters is that the body of put is a buffer
    const actualResponse = await provider.putBuffer('endpoint/123', buffer, {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/octet-stream' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint/123',
      {
        method: 'PUT',
        body: buffer,
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/octet-stream',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
  });

  it('patch with query params', async context => {
    const response = new Response('{"data": "value"}', {
      status: 201,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.patch(
      '/endpoint/123',
      {
        data: 'updatedItemInfo',
      },
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
        queryParams: { param1: 'value1', param2: 'value2' },
        additionnalheaders: { 'X-Additional-Header': 'value1' },
      },
    );

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint/123?param1=value1&param2=value2',
      {
        method: 'PATCH',
        body: JSON.stringify({ data: 'updatedItemInfo' }),
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
  });

  it('delete', async context => {
    const response = new Response(undefined, {
      status: 204,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const actualResponse = await provider.delete('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    });

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint/123',
      {
        method: 'DELETE',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
  });

  it('deleteWithBody', async context => {
    const response = new Response('{"success": true}', {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const requestBody = { webhookIds: [1, 2, 3] };
    const actualResponse = await provider.delete(
      '/webhook',
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
        additionnalheaders: { 'X-Additional-Header': 'value1' },
      },
      requestBody,
    );

    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/webhook',
      {
        method: 'DELETE',
        body: JSON.stringify(requestBody),
        signal: new AbortController().signal,
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { success: true } });
  });

  it('uses rate limiter if provided', async context => {
    const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));

    const rateLimitedProvider = new Provider({
      prepareRequest: requestOptions => {
        return {
          url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
          headers: {
            'X-Custom-Provider-Header': 'value',
            'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
          },
        };
      },
      rateLimiter: mockRateLimiter,
    });

    const response = new Response(undefined, {
      status: 204,
      headers: { 'Content-Type': 'application/json' },
    });

    const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const options = {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    };

    const actualResponse = await rateLimitedProvider.delete('/endpoint/123', options);

    assert.equal(mockRateLimiter.mock.calls.length, 1);
    assert.deepEqual(mockRateLimiter.mock.calls[0]?.arguments[0]?.credentials, options.credentials);
    assert.equal(fetchMock.mock.calls.length, 1);
    assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
      'www.myApi.com/endpoint/123',
      {
        method: 'DELETE',
        body: null,
        signal: new AbortController().signal,
        headers: {
          Accept: 'application/json',
          'X-Custom-Provider-Header': 'value',
          'X-Provider-Credential-Header': 'apikey#1111',
          'X-Additional-Header': 'value1',
        },
      },
    ]);
    assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
  });

  it('uses custom error handler if provided', async context => {
    const rateLimitedProvider = new Provider({
      prepareRequest: requestOptions => {
        return {
          url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
          headers: {
            'X-Custom-Provider-Header': 'value',
            'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
          },
        };
      },
      rateLimiter: undefined,
      // Change from normal behavior 400 -> 429
      customErrorHandler: (responseStatus: number) =>
        responseStatus === 400 ? new HttpErrors.RateLimitExceededError('Weird provider behavior') : undefined,
    });

    const response = new Response(undefined, {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const options = {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    };

    let error;
    try {
      await rateLimitedProvider.delete('/endpoint/123', options);
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.equal(error.message, 'Weird provider behavior');
  });

  it('contains the credential in the custom error handler', async context => {
    const provider = new Provider({
      prepareRequest: requestOptions => {
        return {
          url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
          headers: {
            'X-Custom-Provider-Header': 'value',
            'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
          },
        };
      },
      rateLimiter: undefined,
      customErrorHandler: (responseStatus: number, _message: string, options) => {
        if (responseStatus === 400) {
          // What matter is that we have access to the context in the error handler
          throw new HttpErrors.BadRequestError(`Error with API key ${options?.credentials.apiKey}`);
        }

        return undefined;
      },
    });

    const response = new Response(undefined, {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const options = {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    };

    let error;
    try {
      await provider.delete('/endpoint/123', options);
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.equal(error.message, 'Error with API key apikey#1111');
  });

  it('uses default behavior if custom error handler returns undefined', async context => {
    const rateLimitedProvider = new Provider({
      prepareRequest: requestOptions => {
        return {
          url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
          headers: {
            'X-Custom-Provider-Header': 'value',
            'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
          },
        };
      },
      rateLimiter: undefined,
      // Custom Error Handler returning undefined (default behavior should apply)
      customErrorHandler: () => undefined,
    });

    const response = new Response(undefined, {
      status: 404,
      headers: { 'Content-Type': 'application/json' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const options = {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
      additionnalheaders: { 'X-Additional-Header': 'value1' },
    };

    let error;
    try {
      await rateLimitedProvider.delete('/endpoint/123', options);
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.equal(error.message, 'Not found');
  });

  it('returns valid json response', async context => {
    const response = new Response(`{ "validJson": true }`, {
      status: 200,
      headers: { 'Content-Type': 'application/json;charset=utf-8' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
    });

    assert.ok(providerResponse);
    assert.ok(providerResponse.body);
    assert.equal(providerResponse.body.validJson, true);
  });

  it('returns successfully on missing Content-Type header', async context => {
    const response = new Response(undefined, {
      status: 201,
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
    });

    assert.ok(providerResponse);
    assert.equal(providerResponse.body, undefined);
  });

  it('returns streamable response on streaming get calls', async context => {
    const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
      status: 200,
      headers: { 'Content-Type': 'video/mp4' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const providerResponse = await provider.streamingGet('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      logger: logger,
      signal: new AbortController().signal,
    });

    assert.ok(providerResponse);
    assert.ok(providerResponse.body instanceof ReadableStream);
  });

  it('returns successfully on unexpected content-type response with no body', async context => {
    const response = new Response(null, {
      status: 201,
      headers: { 'Content-Type': 'html/text' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const providerResponse = await provider.post(
      '/endpoint/123',
      {},
      {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
      },
    );

    assert.ok(providerResponse);
    assert.strictEqual(providerResponse.status, response.status);
    assert.strictEqual(providerResponse.headers, response.headers);
    assert.strictEqual(providerResponse.body, undefined);
  });

  it('throws on invalid json response', async context => {
    const response = new Response('{invalidJSON}', {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.equal(error.message, 'Invalid JSON response');
  });

  it('throws on unexpected content-type response', async context => {
    const response = new Response('text', {
      status: 200,
      headers: { 'Content-Type': 'application/text' },
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        logger: logger,
        signal: new AbortController().signal,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.equal(error.status, 500);
  });

  it('throws on status 400', async context => {
    const response = new Response('response body', {
      status: 400,
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        signal: new AbortController().signal,
        logger: logger,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.BadRequestError);
    assert.equal(error.message, 'response body');
  });

  it('throws on timeout', async context => {
    context.mock.method(global, 'fetch', () => {
      const error = new Error();
      error.name = 'TimeoutError';
      throw error;
    });

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        signal: new AbortController().signal,
        logger: logger,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.TimeoutError);
    assert.equal(error.message, 'Request timeout');
  });

  it('throws on abort', async context => {
    context.mock.method(global, 'fetch', () => {
      const error = new Error();
      error.name = 'AbortError';
      throw error;
    });

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        signal: new AbortController().signal,
        logger: logger,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.TimeoutError);
    assert.equal(error.message, 'Request aborted');
  });

  it('throws on unknown errors', async context => {
    context.mock.method(global, 'fetch', () => {
      throw new TypeError('foo', { cause: new Error('bar') });
    });

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        signal: new AbortController().signal,
        logger: logger,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.HttpError);
    assert.ok(error.message.startsWith('Unexpected error while calling the provider.'));
    assert.ok(error.message.includes('ErrorName: "TypeError"'));
    assert.ok(error.message.includes('message: "foo"'));
    assert.ok(error.message.includes('stack:'));
    assert.ok(error.message.includes('cause:'));
    assert.ok(error.message.includes('causeStack:'));
  });

  it('throws on status 429', async context => {
    const response = new Response('response body', {
      status: 429,
    });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    let error;

    try {
      await provider.get('/endpoint/123', {
        credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
        signal: new AbortController().signal,
        logger: logger,
      });
    } catch (e) {
      error = e;
    }

    assert.ok(error instanceof HttpErrors.RateLimitExceededError);
    assert.equal(error.message, 'response body');
  });

  it('logs provider requests', async context => {
    const response = new Response(undefined, { status: 201 });

    context.mock.method(global, 'fetch', () => Promise.resolve(response));

    const loggerStub = context.mock.method(logger, 'info');

    await provider.get('/endpoint/123', {
      credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
      signal: new AbortController().signal,
      logger: logger,
    });

    assert.equal(loggerStub.mock.callCount(), 1);
    assert.match(
      String(loggerStub.mock.calls[0]?.arguments[0]),
      /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/,
    );
  });
});
