// Mock logger before requiring modules
jest.mock('../../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
  debug: jest.fn(),
}));

// Mock timers to prevent the cleanup interval from keeping Jest running
jest.useFakeTimers();

describe('Message Socket', () => {
  let messageSocket;
  let mockIo;
  let mockSocket;
  let connectionHandler;

  beforeEach(() => {
    jest.clearAllMocks();
    jest.clearAllTimers();

    // Reset the module to clear the typingStatus Map
    jest.resetModules();
    messageSocket = require('../../../sockets/messageSocket');

    // Create mock socket
    mockSocket = {
      id: 'socket-123',
      userId: 'user-1',
      user: {
        id: 'user-1',
        email: 'user1@example.com',
        firstName: 'John',
      },
      join: jest.fn(),
      leave: jest.fn(),
      emit: jest.fn(),
      on: jest.fn(),
    };

    // Create mock io
    mockIo = {
      on: jest.fn((event, handler) => {
        if (event === 'connection') {
          connectionHandler = handler;
        }
      }),
      to: jest.fn().mockReturnThis(),
      emit: jest.fn(),
    };
  });

  describe('getConversationRoom', () => {
    it('should generate consistent room name regardless of user order', () => {
      const room1 = messageSocket.getConversationRoom('user-a', 'user-b');
      const room2 = messageSocket.getConversationRoom('user-b', 'user-a');

      expect(room1).toBe(room2);
      expect(room1).toMatch(/^conv_/);
    });

    it('should sort user IDs alphabetically', () => {
      const room = messageSocket.getConversationRoom('zebra', 'alpha');
      expect(room).toBe('conv_alpha_zebra');
    });
  });

  describe('getUserRoom', () => {
    it('should generate user room name', () => {
      const room = messageSocket.getUserRoom('user-123');
      expect(room).toBe('user_user-123');
    });
  });

  describe('initializeMessageSocket', () => {
    it('should register connection handler', () => {
      messageSocket.initializeMessageSocket(mockIo);

      expect(mockIo.on).toHaveBeenCalledWith('connection', expect.any(Function));
    });

    describe('connection handler', () => {
      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        // Trigger the connection handler
        connectionHandler(mockSocket);
      });

      it('should join user personal room on connection', () => {
        expect(mockSocket.join).toHaveBeenCalledWith('user_user-1');
      });

      it('should register event handlers', () => {
        expect(mockSocket.on).toHaveBeenCalledWith('join_conversation', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('leave_conversation', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('typing_start', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('typing_stop', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('mark_message_read', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function));
        expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
      });
    });

    describe('join_conversation event', () => {
      let joinConversationHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        // Get the join_conversation handler
        joinConversationHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'join_conversation'
        )[1];
      });

      it('should join conversation room and emit confirmation', () => {
        joinConversationHandler({ otherUserId: 'user-2' });

        expect(mockSocket.join).toHaveBeenCalledWith('conv_user-1_user-2');
        expect(mockSocket.emit).toHaveBeenCalledWith('conversation_joined', {
          conversationRoom: 'conv_user-1_user-2',
          otherUserId: 'user-2',
        });
      });

      it('should not join if otherUserId is missing', () => {
        mockSocket.join.mockClear();
        mockSocket.emit.mockClear();

        joinConversationHandler({});

        expect(mockSocket.join).not.toHaveBeenCalled();
        expect(mockSocket.emit).not.toHaveBeenCalled();
      });
    });

    describe('leave_conversation event', () => {
      let leaveConversationHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        leaveConversationHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'leave_conversation'
        )[1];
      });

      it('should leave conversation room', () => {
        leaveConversationHandler({ otherUserId: 'user-2' });

        expect(mockSocket.leave).toHaveBeenCalledWith('conv_user-1_user-2');
      });

      it('should not leave if otherUserId is missing', () => {
        leaveConversationHandler({});

        expect(mockSocket.leave).not.toHaveBeenCalled();
      });
    });

    describe('typing_start event', () => {
      let typingStartHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        typingStartHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'typing_start'
        )[1];
      });

      it('should emit typing indicator to receiver', () => {
        typingStartHandler({ receiverId: 'user-2' });

        expect(mockIo.to).toHaveBeenCalledWith('user_user-2');
        expect(mockIo.emit).toHaveBeenCalledWith('user_typing', {
          userId: 'user-1',
          firstName: 'John',
          isTyping: true,
        });
      });

      it('should throttle rapid typing events', () => {
        typingStartHandler({ receiverId: 'user-2' });
        mockIo.emit.mockClear();

        // Should be throttled
        typingStartHandler({ receiverId: 'user-2' });
        expect(mockIo.emit).not.toHaveBeenCalled();

        // Advance time past throttle
        jest.advanceTimersByTime(1001);
        typingStartHandler({ receiverId: 'user-2' });
        expect(mockIo.emit).toHaveBeenCalled();
      });

      it('should not emit if receiverId is missing', () => {
        typingStartHandler({});

        expect(mockIo.to).not.toHaveBeenCalled();
      });
    });

    describe('typing_stop event', () => {
      let typingStopHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        typingStopHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'typing_stop'
        )[1];
      });

      it('should emit typing stop to receiver', () => {
        typingStopHandler({ receiverId: 'user-2' });

        expect(mockIo.to).toHaveBeenCalledWith('user_user-2');
        expect(mockIo.emit).toHaveBeenCalledWith('user_typing', {
          userId: 'user-1',
          firstName: 'John',
          isTyping: false,
        });
      });

      it('should not emit if receiverId is missing', () => {
        typingStopHandler({});

        expect(mockIo.to).not.toHaveBeenCalled();
      });
    });

    describe('mark_message_read event', () => {
      let markMessageReadHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        markMessageReadHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'mark_message_read'
        )[1];
      });

      it('should emit message_read to sender room', () => {
        const data = { messageId: 'msg-123', senderId: 'user-2' };
        markMessageReadHandler(data);

        expect(mockIo.to).toHaveBeenCalledWith('user_user-2');
        expect(mockIo.emit).toHaveBeenCalledWith('message_read', {
          messageId: 'msg-123',
          readAt: expect.any(String),
          readBy: 'user-1',
        });
      });

      it('should not emit if messageId is missing', () => {
        markMessageReadHandler({ senderId: 'user-2' });

        expect(mockIo.to).not.toHaveBeenCalled();
      });

      it('should not emit if senderId is missing', () => {
        markMessageReadHandler({ messageId: 'msg-123' });

        expect(mockIo.to).not.toHaveBeenCalled();
      });
    });

    describe('disconnect event', () => {
      let disconnectHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        disconnectHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'disconnect'
        )[1];
      });

      it('should handle disconnect', () => {
        const logger = require('../../../utils/logger');

        disconnectHandler('client disconnect');

        expect(logger.info).toHaveBeenCalledWith(
          'User disconnected from messaging',
          expect.objectContaining({
            socketId: 'socket-123',
            userId: 'user-1',
            reason: 'client disconnect',
          })
        );
      });
    });

    describe('error event', () => {
      let errorHandler;

      beforeEach(() => {
        messageSocket.initializeMessageSocket(mockIo);
        connectionHandler(mockSocket);

        errorHandler = mockSocket.on.mock.calls.find(
          (call) => call[0] === 'error'
        )[1];
      });

      it('should log socket errors', () => {
        const logger = require('../../../utils/logger');
        const error = new Error('Socket error');

        errorHandler(error);

        expect(logger.error).toHaveBeenCalledWith(
          'Socket error',
          expect.objectContaining({
            socketId: 'socket-123',
            userId: 'user-1',
            error: 'Socket error',
          })
        );
      });
    });
  });

  describe('emitNewMessage', () => {
    it('should emit new_message to receiver room', () => {
      const messageData = {
        id: 'msg-456',
        senderId: 'user-1',
        content: 'Hello!',
      };

      messageSocket.emitNewMessage(mockIo, 'user-2', messageData);

      expect(mockIo.to).toHaveBeenCalledWith('user_user-2');
      expect(mockIo.emit).toHaveBeenCalledWith('new_message', messageData);
    });

    it('should handle errors gracefully', () => {
      const logger = require('../../../utils/logger');
      mockIo.to.mockImplementation(() => {
        throw new Error('Emit failed');
      });

      const messageData = { id: 'msg-456', senderId: 'user-1' };
      messageSocket.emitNewMessage(mockIo, 'user-2', messageData);

      expect(logger.error).toHaveBeenCalledWith(
        'Error emitting new message',
        expect.objectContaining({
          error: 'Emit failed',
        })
      );
    });
  });

  describe('emitMessageRead', () => {
    it('should emit message_read to sender room', () => {
      const readData = {
        messageId: 'msg-789',
        readAt: '2024-01-01T00:00:00Z',
        readBy: 'user-2',
      };

      messageSocket.emitMessageRead(mockIo, 'user-1', readData);

      expect(mockIo.to).toHaveBeenCalledWith('user_user-1');
      expect(mockIo.emit).toHaveBeenCalledWith('message_read', readData);
    });

    it('should handle errors gracefully', () => {
      const logger = require('../../../utils/logger');
      mockIo.to.mockImplementation(() => {
        throw new Error('Emit failed');
      });

      const readData = { messageId: 'msg-789' };
      messageSocket.emitMessageRead(mockIo, 'user-1', readData);

      expect(logger.error).toHaveBeenCalledWith(
        'Error emitting message read status',
        expect.objectContaining({
          error: 'Emit failed',
        })
      );
    });
  });
});
