/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test, beforeEach, jest, expect } from '@jest/globals';
import streamtest from 'streamtest';
import { YError } from 'yerror';
import initHTTPTransaction from './httpTransaction.js';
import { type WhookAPMService } from './apm.js';
import { type WhookObfuscatorService } from './obfuscator.js';
import { ServerResponse, type IncomingMessage } from 'node:http';
import {
  type LogService,
  type TimeService,
  type DelayService,
} from 'common-services';
import { type WhookResponse } from '../types/http.js';

function streamifyBody(response: WhookResponse): WhookResponse {
  return response
    ? Object.assign({}, response, {
        body: streamtest.fromChunks([
          Buffer.from(JSON.stringify(response.body)),
        ]),
      })
    : { status: 418 };
}

describe('initHTTPTransaction', () => {
  const log = jest.fn<LogService>();
  const apm = jest.fn<WhookAPMService>();
  const time = jest.fn<TimeService>();
  const uniqueId = jest.fn<() => string>();
  const delay = {
    create: jest.fn<DelayService['create']>(),
    clear: jest.fn<DelayService['clear']>(),
  };
  const obfuscator = {
    obfuscate: jest.fn<WhookObfuscatorService['obfuscate']>(),
    obfuscateSensibleProps:
      jest.fn<WhookObfuscatorService['obfuscateSensibleProps']>(),
    obfuscateSensibleHeaders:
      jest.fn<WhookObfuscatorService['obfuscateSensibleHeaders']>(),
  };

  beforeEach(() => {
    log.mockReset();
    apm.mockReset();
    time.mockReset();
    uniqueId.mockReset();
    delay.create.mockReset();
    delay.clear.mockReset();
    obfuscator.obfuscate.mockReset();
    obfuscator.obfuscateSensibleProps.mockReset();
    obfuscator.obfuscateSensibleHeaders.mockReset();
  });

  test('should work', async () => {
    const httpTransaction = await initHTTPTransaction({
      obfuscator,
      log,
      apm,
      time,
      delay,
      uniqueId,
    });

    expect(httpTransaction).toBeTruthy();
    expect({
      logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
      apmCalls: apm.mock.calls,
      timeCalls: time.mock.calls,
      uniqueIdCalls: uniqueId.mock.calls,
      delayCreateCalls: delay.create.mock.calls,
      delayClearCalls: delay.clear.mock.calls,
      obfuscateCalls: obfuscator.obfuscate.mock.calls,
      obfuscateSensiblePropsCalls: obfuscator.obfuscateSensibleProps.mock.calls,
      obfuscateSensibleHeadersCalls:
        obfuscator.obfuscateSensibleHeaders.mock.calls,
    }).toMatchSnapshot();
  });

  describe('httpTransaction', () => {
    const buildResponse = jest.fn<any>();

    beforeEach(() => {
      buildResponse.mockReset();
    });

    test('should work', async () => {
      time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
      uniqueId.mockReturnValue('[id]');
      delay.create.mockReturnValueOnce(new Promise(() => undefined));
      delay.clear.mockResolvedValue('cancel');

      const httpTransaction = await initHTTPTransaction({
        obfuscator,
        log,
        apm,
        time,
        delay,
        uniqueId,
      });
      const req = {
        connection: { encrypted: true },
        ts: 1000000,
        ip: '127.0.0.1',
        method: 'GET',
        url: '/v1/users/1?extended=true',
        headers: {
          'x-forwarded-for': '127.0.0.1',
        },
        socket: {
          bytesRead: 16,
          bytesWritten: 64,
        },
      };
      const [res, resBodyPromise] = streamtest.toText() as [
        ServerResponse<IncomingMessage>,
        Promise<string>,
      ];
      const writeHead = jest.fn<ServerResponse<IncomingMessage>['writeHead']>();

      res.writeHead = writeHead as ServerResponse<IncomingMessage>['writeHead'];

      buildResponse.mockResolvedValueOnce({
        body: streamtest.fromChunks([
          Buffer.from(
            JSON.stringify({
              id: 1,
              name: 'John Doe',
            }),
          ),
        ]),
        headers: {
          'Content-Type': 'application/json',
        },
        status: 200,
      });

      const { request, transaction } = await httpTransaction(
        req as unknown as IncomingMessage,
        res,
      );

      const response = await transaction.start(buildResponse);

      await transaction.end(response, 'theOperationId');

      expect(request.url).toEqual('/v1/users/1?extended=true');
      expect(request.method).toEqual('get');
      expect(request.headers).toEqual({
        'x-forwarded-for': '127.0.0.1',
      });
      expect(request.body).toEqual(req);
      expect(writeHead.mock.calls).toEqual([
        [
          200,
          'OK',
          {
            'Content-Type': 'application/json',
            'Transaction-Id': '[id]',
          },
        ],
      ]);

      const text = await resBodyPromise;

      expect(text).toEqual('{"id":1,"name":"John Doe"}');
      expect({
        request,
        buildResponseCalls: buildResponse.mock.calls,
        logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
        apmCalls: apm.mock.calls,
        timeCalls: time.mock.calls,
        uniqueIdCalls: uniqueId.mock.calls,
        delayCreateCalls: delay.create.mock.calls,
        delayClearCalls: delay.clear.mock.calls,
        obfuscateCalls: obfuscator.obfuscate.mock.calls,
        obfuscateSensiblePropsCalls:
          obfuscator.obfuscateSensibleProps.mock.calls,
        obfuscateSensibleHeadersCalls:
          obfuscator.obfuscateSensibleHeaders.mock.calls,
      }).toMatchSnapshot();
    });

    test('should fail on timeout', async () => {
      time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
      uniqueId.mockReturnValue('[id]');
      delay.create.mockResolvedValueOnce('timeout');
      delay.clear.mockResolvedValueOnce('timeout');

      let request;

      try {
        const httpTransaction = await initHTTPTransaction({
          obfuscator,
          log,
          apm,
          time,
          delay,
          uniqueId,
        });
        const req = {
          connection: { encrypted: true },
          ts: 1000000,
          ip: '127.0.0.1',
          method: 'GET',
          url: '/v1/users/1?extended=true',
          headers: {
            'x-forwarded-for': '127.0.0.1',
          },
          socket: {
            bytesRead: 16,
            bytesWritten: 64,
          },
        };
        const [res, resBodyPromise] = streamtest.toText() as [
          ServerResponse<IncomingMessage>,
          Promise<string>,
        ];
        const writeHead =
          jest.fn<ServerResponse<IncomingMessage>['writeHead']>();

        res.writeHead =
          writeHead as ServerResponse<IncomingMessage>['writeHead'];

        buildResponse.mockReturnValueOnce(new Promise(() => undefined));

        const { request: _request, transaction } = await httpTransaction(
          req as unknown as IncomingMessage,
          res,
        );

        request = _request;

        await transaction.start(buildResponse);

        await transaction.end({ status: 200 });

        await resBodyPromise;

        throw new YError('E_UNEXPECTED_SUCCESS');
      } catch (err) {
        expect((err as any).code).toEqual('E_TRANSACTION_TIMEOUT');
        expect((err as any).httpCode).toEqual(504);
        expect({
          request,
          logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
          apmCalls: apm.mock.calls,
          timeCalls: time.mock.calls,
          uniqueIdCalls: uniqueId.mock.calls,
          delayCreateCalls: delay.create.mock.calls,
          delayClearCalls: delay.clear.mock.calls,
          obfuscateCalls: obfuscator.obfuscate.mock.calls,
          obfuscateSensiblePropsCalls:
            obfuscator.obfuscateSensibleProps.mock.calls,
          obfuscateSensibleHeadersCalls:
            obfuscator.obfuscateSensibleHeaders.mock.calls,
        }).toMatchSnapshot();
      }
    });

    test('should fail with non-unique transaction id', async () => {
      time.mockReturnValue(new Date('2012-01-15T00:00:00.000Z').getTime());
      uniqueId.mockReturnValue('[id]');
      delay.create.mockReturnValueOnce(new Promise(() => 'cancel'));
      delay.clear.mockResolvedValue('cancel');

      try {
        const httpTransaction = await initHTTPTransaction({
          obfuscator,
          log,
          apm,
          time,
          delay,
          uniqueId,
          TRANSACTIONS: { lol: {} },
        });
        const req = {
          connection: { encrypted: true },
          ts: 1000000,
          ip: '127.0.0.1',
          method: 'GET',
          url: '/v1/users/1?extended=true',
          headers: {
            'x-forwarded-for': '127.0.0.1',
            'transaction-id': 'lol',
          },
          socket: {
            bytesRead: 16,
            bytesWritten: 64,
          },
        };
        const [res, resBodyPromise] = streamtest.toText() as [
          ServerResponse<IncomingMessage>,
          Promise<string>,
        ];
        const writeHead =
          jest.fn<ServerResponse<IncomingMessage>['writeHead']>();

        res.writeHead =
          writeHead as ServerResponse<IncomingMessage>['writeHead'];

        buildResponse.mockReturnValueOnce(Promise.resolve());

        const { transaction } = await httpTransaction(
          req as unknown as IncomingMessage,
          res,
        );

        await transaction
          .start(buildResponse)
          .catch(transaction.catch)
          .then(streamifyBody)
          .then(transaction.end);
        await resBodyPromise;

        throw new YError('E_UNEXPECTED_SUCCESS');
      } catch (err) {
        expect((err as any).code).toEqual('E_TRANSACTION_ID_NOT_UNIQUE');
        expect((err as any).httpCode).toEqual(400);
        expect({
          logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
          timeCalls: time.mock.calls,
          uniqueIdCalls: uniqueId.mock.calls,
          delayCreateCalls: delay.create.mock.calls,
          delayClearCalls: delay.clear.mock.calls,
          obfuscateCalls: obfuscator.obfuscate.mock.calls,
          obfuscateSensiblePropsCalls:
            obfuscator.obfuscateSensibleProps.mock.calls,
          obfuscateSensibleHeadersCalls:
            obfuscator.obfuscateSensibleHeaders.mock.calls,
        }).toMatchSnapshot();
      }
    });
  });
});
