import type { NotificationSentEvent } from '../../domain/events/notification-sent.event';
import { NotificationsGateway } from '../gateways/notifications.gateway';

function createMockSocket(overrides: Partial<{
  id: string;
  data: Record<string, unknown>;
  handshake: { auth?: Record<string, unknown>; headers?: Record<string, unknown>; query?: Record<string, unknown> };
  join: ReturnType<typeof vi.fn>;
  emit: ReturnType<typeof vi.fn>;
  disconnect: ReturnType<typeof vi.fn>;
}> = {}) {
  return {
    id: overrides.id ?? 'socket-1',
    data: overrides.data ?? {},
    handshake: overrides.handshake ?? { auth: { token: 'valid-jwt' }, headers: {}, query: {} },
    join: overrides.join ?? vi.fn().mockResolvedValue(undefined),
    emit: overrides.emit ?? vi.fn(),
    disconnect: overrides.disconnect ?? vi.fn(),
  } as any;
}

describe('NotificationsGateway', () => {
  let gateway: NotificationsGateway;
  let mockTokenService: { verifyAccessToken: ReturnType<typeof vi.fn> };
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    debug: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };
  let mockRedisService: {
    isAvailable: ReturnType<typeof vi.fn>;
    get: ReturnType<typeof vi.fn>;
    set: ReturnType<typeof vi.fn>;
    del: ReturnType<typeof vi.fn>;
    getClient: ReturnType<typeof vi.fn>;
  };
  let mockNotificationRepo: { countUnreadByUserId: ReturnType<typeof vi.fn> };
  let mockMetrics: {
    recordWsConnection: ReturnType<typeof vi.fn>;
    setWsConnectedClients: ReturnType<typeof vi.fn>;
    recordWsMessage: ReturnType<typeof vi.fn>;
  };
  let mockServer: {
    to: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockTokenService = {
      verifyAccessToken: vi.fn().mockReturnValue({ sub: 'user-1', role: 'USER' }),
    };
    mockLogger = { log: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() };
    mockRedisService = {
      isAvailable: vi.fn().mockReturnValue(true),
      get: vi.fn().mockResolvedValue(null),
      set: vi.fn().mockResolvedValue(undefined),
      del: vi.fn().mockResolvedValue(undefined),
      getClient: vi.fn().mockReturnValue({ exists: vi.fn().mockResolvedValue(0), incr: vi.fn() }),
    };
    mockNotificationRepo = { countUnreadByUserId: vi.fn().mockResolvedValue(3) };
    mockMetrics = {
      recordWsConnection: vi.fn(),
      setWsConnectedClients: vi.fn(),
      recordWsMessage: vi.fn(),
    };

    gateway = new NotificationsGateway(
      mockTokenService as any,
      mockLogger as any,
      mockRedisService as any,
      mockMetrics as any,
      mockNotificationRepo as any,
    );

    // Wire the server mock
    mockServer = { to: vi.fn().mockReturnValue({ emit: vi.fn() }) };
    (gateway as any).server = mockServer;
  });

  describe('afterInit', () => {
    it('logs initialization', () => {
      gateway.afterInit();
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('initialized'),
        'NotificationsGateway',
      );
    });

    it('resets the WS connected-clients gauge to 0', () => {
      gateway.afterInit();
      expect(mockMetrics.setWsConnectedClients).toHaveBeenCalledWith(
        '/notifications',
        0,
      );
    });
  });

  describe('handleConnection', () => {
    it('authenticates, joins room, and emits unread count', async () => {
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(mockTokenService.verifyAccessToken).toHaveBeenCalledWith('valid-jwt');
      expect(socket.data['userId']).toBe('user-1');
      expect(socket.data['role']).toBe('USER');
      expect(socket.join).toHaveBeenCalledWith('user:user-1');
      expect(socket.emit).toHaveBeenCalledWith('notification:unread-count', { unreadCount: 3 });
    });

    it('strips Bearer prefix from Authorization header', async () => {
      const socket = createMockSocket({
        handshake: { auth: {}, headers: { authorization: 'Bearer my-token' }, query: {} },
      });

      await gateway.handleConnection(socket);

      expect(mockTokenService.verifyAccessToken).toHaveBeenCalledWith('my-token');
    });

    it('disconnects client when no token provided', async () => {
      const socket = createMockSocket({
        handshake: { auth: {}, headers: {}, query: {} },
      });
      mockTokenService.verifyAccessToken.mockReturnValue(null);

      await gateway.handleConnection(socket);

      expect(socket.disconnect).toHaveBeenCalledWith(true);
    });

    it('disconnects client when token is invalid', async () => {
      mockTokenService.verifyAccessToken.mockReturnValue(null);
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(socket.disconnect).toHaveBeenCalledWith(true);
    });

    it('tracks multiple sockets per user (multi-device)', async () => {
      const socket1 = createMockSocket({ id: 'sock-a' });
      const socket2 = createMockSocket({ id: 'sock-b' });

      await gateway.handleConnection(socket1);
      await gateway.handleConnection(socket2);

      // Both sockets tracked
      const userSockets = (gateway as any).userSockets as Map<string, Set<string>>;
      expect(userSockets.get('user-1')?.size).toBe(2);
      expect(userSockets.get('user-1')?.has('sock-a')).toBe(true);
      expect(userSockets.get('user-1')?.has('sock-b')).toBe(true);
    });

    it('uses cached unread count from Redis when available', async () => {
      mockRedisService.get.mockResolvedValue('7');
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(socket.emit).toHaveBeenCalledWith('notification:unread-count', { unreadCount: 7 });
      expect(mockNotificationRepo.countUnreadByUserId).not.toHaveBeenCalled();
    });

    it('falls back to DB when Redis unavailable', async () => {
      mockRedisService.isAvailable.mockReturnValue(false);
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(mockNotificationRepo.countUnreadByUserId).toHaveBeenCalledWith('user-1');
      expect(socket.emit).toHaveBeenCalledWith('notification:unread-count', { unreadCount: 3 });
    });

    it('increments WS connection metric and records the initial unread-count emit', async () => {
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(mockMetrics.recordWsConnection).toHaveBeenCalledWith('/notifications', 1);
      expect(mockMetrics.recordWsMessage).toHaveBeenCalledWith(
        '/notifications',
        'notification:unread-count',
        'out',
      );
    });

    it('does not increment metrics when auth fails', async () => {
      mockTokenService.verifyAccessToken.mockReturnValue(null);
      const socket = createMockSocket();

      await gateway.handleConnection(socket);

      expect(mockMetrics.recordWsConnection).not.toHaveBeenCalled();
    });
  });

  describe('handleDisconnect', () => {
    it('removes socket from tracking map', async () => {
      const socket = createMockSocket({ id: 'sock-1' });
      await gateway.handleConnection(socket);

      gateway.handleDisconnect(socket);

      const userSockets = (gateway as any).userSockets as Map<string, Set<string>>;
      expect(userSockets.has('user-1')).toBe(false);
    });

    it('keeps other sockets when one disconnects', async () => {
      const socket1 = createMockSocket({ id: 'sock-1' });
      const socket2 = createMockSocket({ id: 'sock-2' });
      await gateway.handleConnection(socket1);
      await gateway.handleConnection(socket2);

      gateway.handleDisconnect(socket1);

      const userSockets = (gateway as any).userSockets as Map<string, Set<string>>;
      expect(userSockets.get('user-1')?.size).toBe(1);
      expect(userSockets.get('user-1')?.has('sock-2')).toBe(true);
    });

    it('handles disconnect from unknown socket gracefully', () => {
      const socket = createMockSocket();
      // No prior connection — should not throw
      expect(() => gateway.handleDisconnect(socket)).not.toThrow();
    });

    it('decrements the WS connection metric when a tracked socket disconnects', async () => {
      const socket = createMockSocket({ id: 'sock-1' });
      await gateway.handleConnection(socket);
      mockMetrics.recordWsConnection.mockClear();

      gateway.handleDisconnect(socket);

      expect(mockMetrics.recordWsConnection).toHaveBeenCalledWith('/notifications', -1);
    });

    it('does not decrement the gauge for untracked sockets', () => {
      const socket = createMockSocket();

      gateway.handleDisconnect(socket);

      expect(mockMetrics.recordWsConnection).not.toHaveBeenCalled();
    });
  });

  describe('handleNotificationSent', () => {
    const event: NotificationSentEvent = {
      aggregateId: 'notif-1',
      userId: 'user-1',
      templateKey: 'listing_approved',
      channel: 'EMAIL',
      occurredAt: new Date('2026-04-16T12:00:00Z'),
    } as any;

    it('emits notification:new to user room', async () => {
      const roomEmit = vi.fn();
      mockServer.to.mockReturnValue({ emit: roomEmit });

      await gateway.handleNotificationSent(event);

      expect(mockServer.to).toHaveBeenCalledWith('user:user-1');
      expect(roomEmit).toHaveBeenCalledWith('notification:new', {
        id: 'notif-1',
        templateKey: 'listing_approved',
        channel: 'EMAIL',
        occurredAt: '2026-04-16T12:00:00.000Z',
      });
    });

    it('emits updated unread count after notification', async () => {
      const roomEmit = vi.fn();
      mockServer.to.mockReturnValue({ emit: roomEmit });

      await gateway.handleNotificationSent(event);

      // Called twice: once for notification:new, once for unread-count
      expect(roomEmit).toHaveBeenCalledWith('notification:unread-count', { unreadCount: 3 });
    });

    it('increments cached unread count in Redis when key exists', async () => {
      const mockIncr = vi.fn();
      mockRedisService.getClient.mockReturnValue({
        exists: vi.fn().mockResolvedValue(1),
        incr: mockIncr,
      });
      mockServer.to.mockReturnValue({ emit: vi.fn() });

      await gateway.handleNotificationSent(event);

      expect(mockIncr).toHaveBeenCalled();
    });

    it('does not throw when event handling fails', async () => {
      mockServer.to.mockImplementation(() => {
        throw new Error('server error');
      });

      await expect(gateway.handleNotificationSent(event)).resolves.not.toThrow();
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.stringContaining('Failed to emit'),
        expect.any(String),
        'NotificationsGateway',
      );
    });
  });

  describe('emitUnreadCount', () => {
    it('emits unread count to user room', async () => {
      const roomEmit = vi.fn();
      mockServer.to.mockReturnValue({ emit: roomEmit });

      await gateway.emitUnreadCount('user-1');

      expect(mockServer.to).toHaveBeenCalledWith('user:user-1');
      expect(roomEmit).toHaveBeenCalledWith('notification:unread-count', { unreadCount: 3 });
    });
  });

  describe('invalidateUnreadCount', () => {
    it('deletes cached unread count from Redis', async () => {
      await gateway.invalidateUnreadCount('user-1');

      expect(mockRedisService.del).toHaveBeenCalledWith('notifications:unread:user-1');
    });

    it('skips deletion when Redis unavailable', async () => {
      mockRedisService.isAvailable.mockReturnValue(false);

      await gateway.invalidateUnreadCount('user-1');

      expect(mockRedisService.del).not.toHaveBeenCalled();
    });
  });

  describe('emitResidentialEvent', () => {
    it('emits the residential event to the user room and records a ws metric', () => {
      const roomEmit = vi.fn();
      mockServer.to.mockReturnValue({ emit: roomEmit });

      gateway.emitResidentialEvent('user-42', 'residential:price-drop', {
        listingId: 'listing-1',
      });

      expect(mockServer.to).toHaveBeenCalledWith('user:user-42');
      expect(roomEmit).toHaveBeenCalledWith('residential:price-drop', {
        listingId: 'listing-1',
      });
      expect(mockMetrics.recordWsMessage).toHaveBeenCalledWith(
        '/notifications',
        'residential:price-drop',
        'out',
      );
    });
  });
});
