import { BOOKING_WINDOW_WS_EVENTS, BOOKING_WINDOW_WS_NAMESPACE } from '@edr/types';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { io, type Socket } from 'socket.io-client';

import { WsAuthService } from '../notification-inbox/ws-auth.service';
import { BookingWindowGateway } from './booking-window.gateway';
import type { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';

/**
 * End-to-end proof the booking-window socket works: boots a real Nest app with
 * the gateway, connects a real socket.io client to the namespace, emits a phase
 * change, and asserts the client receives the exact payload. If this passes,
 * any "no live update" report is environmental (stale server process, wrong
 * checkout running, client not connecting) — not the gateway.
 */
describe('BookingWindowGateway (e2e)', () => {
  let app: INestApplication;
  let gateway: BookingWindowGateway;
  let client: Socket;
  let baseUrl: string;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        BookingWindowGateway,
        // Accept any token — auth plumbing is covered by the real WsAuthService.
        { provide: WsAuthService, useValue: { resolveUserId: async () => 'user-1' } },
      ],
    }).compile();

    app = moduleRef.createNestApplication();
    await app.listen(0);
    const address = app.getHttpServer().address() as { port: number };
    baseUrl = `http://127.0.0.1:${address.port}`;
    gateway = app.get(BookingWindowGateway);
  });

  afterAll(async () => {
    client?.disconnect();
    await app?.close();
  });

  it('authenticated client receives the phase event with the schedule state', async () => {
    client = io(`${baseUrl}/${BOOKING_WINDOW_WS_NAMESPACE}`, {
      auth: { token: 'any' },
      transports: ['websocket'],
    });
    await new Promise<void>((resolve, reject) => {
      client.on('connect', () => resolve());
      client.on('connect_error', (err) => reject(err));
    });

    const received = new Promise<Record<string, unknown>>((resolve) => {
      client.on(BOOKING_WINDOW_WS_EVENTS.PHASE, (payload) => resolve(payload));
    });

    gateway.emitPhase({
      id: 'sched-1',
      originStationId: 'yard-a',
      destinationStationId: 'yard-b',
      direction: 'IMPORT',
      windowPhase: 'OPEN',
      bookingWindowStatus: 'OPEN',
      bookingCycleNo: 2,
      windowOpensAt: new Date('2026-07-06T16:15:00Z'),
      windowClosesAt: new Date('2026-07-06T16:18:00Z'),
      docReviewEndsAt: null,
      paymentPhaseEndsAt: null,
      scheduledDepartureDate: new Date('2026-07-09T05:53:00Z'),
    } as unknown as TrainSchedule);

    const payload = await received;
    expect(payload).toMatchObject({
      scheduleId: 'sched-1',
      phase: 'OPEN',
      bookingWindowStatus: 'OPEN',
      bookingCycleNo: 2,
      windowOpensAt: '2026-07-06T16:15:00.000Z',
    });
  });

  it('rejects a client whose token does not resolve to a user', async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        BookingWindowGateway,
        { provide: WsAuthService, useValue: { resolveUserId: async () => null } },
      ],
    }).compile();
    const rejectingApp = moduleRef.createNestApplication();
    await rejectingApp.listen(0);
    const addr = rejectingApp.getHttpServer().address() as { port: number };

    const rejected = io(`http://127.0.0.1:${addr.port}/${BOOKING_WINDOW_WS_NAMESPACE}`, {
      auth: { token: 'bad' },
      transports: ['websocket'],
      reconnection: false,
    });
    const outcome = await new Promise<string>((resolve) => {
      rejected.on('disconnect', () => resolve('disconnected'));
      rejected.on('connect_error', () => resolve('rejected'));
      // The server accepts the transport then drops it in handleConnection.
      setTimeout(() => resolve(rejected.connected ? 'still-connected' : 'disconnected'), 500);
    });
    rejected.disconnect();
    await rejectingApp.close();
    expect(outcome).not.toBe('still-connected');
  });
});
