import { describe, test, beforeEach, jest, expect } from '@jest/globals';
import initHTTPServer from './httpServer.js';
import axios from 'axios';
import net from 'node:net';
import { YError } from 'yerror';
import { type WhookHTTPRouterService } from './httpRouter.js';
import { type LogService } from 'common-services';
import { type ServerResponse } from 'node:http';

describe('initHTTPServer', () => {
  const PORT = 7777;
  const HOST = 'localhost';
  const log = jest.fn<LogService>();
  const httpRouter = jest.fn<WhookHTTPRouterService>();

  beforeEach(() => {
    log.mockReset();
    httpRouter.mockReset();
  });

  test('should work with keepalive connections', async () => {
    const httpServer = await initHTTPServer({
      HOST,
      PORT,
      log,
      httpRouter,
    });

    if (httpServer.dispose) {
      await httpServer.dispose();
    }

    expect({
      logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
      httpRouterCalls: httpRouter.mock.calls,
    }).toMatchSnapshot();
  });

  test('should work with instantly destroyed connections', async () => {
    const httpServer = await initHTTPServer({
      ENV: {
        DESTROY_SOCKETS: '1',
      },
      HOST,
      PORT,
      httpRouter,
    });

    if (httpServer.dispose) {
      await httpServer.dispose();
    }

    expect({
      logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
      httpRouterCalls: httpRouter.mock.calls,
    }).toMatchSnapshot();
  });

  test('should proxy server fatal errors', async () => {
    const httpServer = await initHTTPServer({
      ENV: {
        DESTROY_SOCKETS: '1',
      },
      HTTP_SERVER_OPTIONS: {
        maxConnections: 4,
      },
      HOST,
      PORT,
      httpRouter,
    });

    try {
      httpServer.service.emit('error', new YError('E_ERROR'));

      await httpServer.fatalErrorPromise;
      throw new YError('E_UNEXPECTED_SUCCESS');
    } catch (err) {
      if (httpServer.dispose) {
        await httpServer.dispose();
      }

      expect({
        errorCode: (err as YError).code,
        errorDebug: (err as YError).debug,
        logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
        httpRouterCalls: httpRouter.mock.calls,
      }).toMatchSnapshot();
    }
  });

  test('should proxy server close errors', async () => {
    const httpServer = await initHTTPServer({
      ENV: {
        DESTROY_SOCKETS: '1',
      },
      HTTP_SERVER_OPTIONS: {
        maxConnections: 4,
      },
      HOST,
      PORT,
      httpRouter,
    });

    try {
      httpServer.service.close = (
        (realClose) => async (cb: (err?: Error) => undefined) => {
          await new Promise<void>((resolve, reject) => {
            realClose((err: Error | undefined) => {
              if (err) {
                reject(err);
                return;
              }
              resolve();
            });
          });

          cb(new YError('E_ERROR'));
        }
      )(
        httpServer.service.close.bind(httpServer.service),
      ) as unknown as typeof httpServer.service.close;

      if (httpServer.dispose) {
        await httpServer.dispose();
      }

      throw new YError('E_UNEXPECTED_SUCCESS');
    } catch (err) {
      expect({
        errorCode: (err as YError).code,
        errorDebug: (err as YError).debug,
        logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
        httpRouterCalls: httpRouter.mock.calls,
      }).toMatchSnapshot();
    }
  });

  test('should close even with opened sockets when using DESTROY_SOCKETS', async () => {
    const httpServer = await initHTTPServer({
      ENV: {
        DESTROY_SOCKETS: '1',
      },
      HOST,
      PORT,
      httpRouter,
    });

    const client = net.createConnection({
      host: HOST,
      port: PORT,
    });

    await new Promise<void>((resolve, reject) => {
      client.on('error', reject);
      client.on('connect', () => resolve());
      client.write('GET / HTTP/1.1\r\n\r\n');
    });

    if (httpServer.dispose) {
      await httpServer.dispose();
    }

    expect({
      logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
      httpRouterCalls: httpRouter.mock.calls,
    }).toMatchSnapshot();
  });

  test('should serve the router', async () => {
    const httpServer = await initHTTPServer({
      ENV: {
        DESTROY_SOCKETS: '1',
      },
      HOST,
      PORT,
      httpRouter: ((_: unknown, res: ServerResponse) => {
        res.writeHead(200, {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
          'Access-Control-Allow-Headers': 'Content-Type',
        });
        res.end();
      }) as unknown as WhookHTTPRouterService,
    });

    const { status } = await axios({
      method: 'get',
      url: `http://${HOST}:${PORT}/`,
      validateStatus: () => true,
    });

    if (httpServer.dispose) {
      await httpServer.dispose();
    }

    expect({
      status,
      logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')),
    }).toMatchSnapshot();
  });
});
