import { INestApplication } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { Test } from '@nestjs/testing';
import { PrismaService } from '@/prisma/prisma.service';
import { MobileSystemNotificationsService } from '../system-notifications/mobile-system-notifications.service';
import { MobileNotificationsGateway } from './mobile-notifications.gateway';
import { MobileTransportService } from './mobile-transport.service';

describe('mobile WebSocket wire protocol', () => {
  let app: INestApplication;
  let port: number;
  let previousSecret: string | undefined;
  let previousEnabled: string | undefined;
  let transport: MobileTransportService;
  const sockets: WebSocket[] = [];
  const sessions = { findFirst: jest.fn() };

  beforeAll(async () => {
    previousSecret = process.env.JWT_SECRET;
    previousEnabled = process.env.MOBILE_REALTIME_ENABLED;
    process.env.JWT_SECRET = 'test-wire-secret';
    process.env.MOBILE_REALTIME_ENABLED = 'true';
    const module = await Test.createTestingModule({
      imports: [JwtModule.register({})],
      providers: [
        MobileNotificationsGateway,
        MobileTransportService,
        { provide: PrismaService, useValue: { userSession: sessions } },
        {
          provide: MobileSystemNotificationsService,
          useValue: {
            findAllForCurrentUser: jest
              .fn()
              .mockResolvedValue({
                data: [{ id: 91, updatedAt: new Date(0) }],
                meta: { total: 1 },
              }),
          },
        },
      ],
    }).compile();
    app = module.createNestApplication();
    await app.listen(0, '127.0.0.1');
    port = app.getHttpServer().address().port;
    transport = app.get(MobileTransportService);
  });
  afterEach(() => {
    for (const socket of sockets.splice(0)) socket.close();
  });
  afterAll(async () => {
    await app?.close();
    if (previousSecret === undefined) delete process.env.JWT_SECRET;
    else process.env.JWT_SECRET = previousSecret;
    if (previousEnabled === undefined)
      delete process.env.MOBILE_REALTIME_ENABLED;
    else process.env.MOBILE_REALTIME_ENABLED = previousEnabled;
  });

  function connect(ticket: string) {
    const socket = new WebSocket(
      `ws://127.0.0.1:${port}/socket.io/?EIO=4&transport=websocket`,
    );
    sockets.push(socket);
    socket.addEventListener('message', (event) => {
      const packet = String(event.data);
      if (packet.startsWith('0'))
        socket.send(`40/mobile-notifications,${JSON.stringify({ ticket })}`);
      if (packet === '2') socket.send('3');
    });
    return socket;
  }

  it('authenticates the mobile namespace and emits a refresh without alert content', async () => {
    sessions.findFirst.mockResolvedValue({ id: 'wire-session' });
    const ticket = transport.ticket({
      userId: 7,
      sessionId: 'wire-session',
    }).ticket;
    const socket = connect(ticket);
    const event = await new Promise<string>((resolve, reject) => {
      const timeout = setTimeout(
        () => reject(new Error('Realtime refresh not received')),
        12000,
      );
      socket.addEventListener('message', (message) => {
        const packet = String(message.data);
        if (packet.includes('notifications.changed')) {
          clearTimeout(timeout);
          resolve(packet);
        }
      });
      socket.addEventListener('error', () => {
        clearTimeout(timeout);
        reject(new Error('WebSocket failed'));
      });
    });
    expect(event).toBe('42/mobile-notifications,["notifications.changed"]');
  }, 15000);

  it('rejects a ticket for a logged-out session on the actual WebSocket connection', async () => {
    sessions.findFirst.mockResolvedValue(null);
    const socket = connect(
      transport.ticket({ userId: 7, sessionId: 'wire-session' }).ticket,
    );
    await new Promise<void>((resolve, reject) => {
      const timeout = setTimeout(
        () => reject(new Error('Revoked connection remained open')),
        5000,
      );
      socket.addEventListener('close', () => {
        clearTimeout(timeout);
        resolve();
      });
    });
  }, 10000);
});
